Merge branch 'master' of github.com:Kava-Labs/kava into fedekunze/fix-cli-tests

This commit is contained in:
Federico Kunze 2020-04-30 09:14:09 -04:00
commit 0ee0dd8c41
No known key found for this signature in database
GPG Key ID: 655F93A970080A30
98 changed files with 5288 additions and 1084 deletions

View File

@ -7,6 +7,7 @@ import (
"github.com/kava-labs/kava/x/auction"
"github.com/kava-labs/kava/x/bep3"
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/incentive"
"github.com/kava-labs/kava/x/kavadist"
"github.com/kava-labs/kava/x/pricefeed"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
@ -69,6 +70,7 @@ var (
pricefeed.AppModuleBasic{},
bep3.AppModuleBasic{},
kavadist.AppModuleBasic{},
incentive.AppModuleBasic{},
)
// module account permissions
@ -121,6 +123,7 @@ type App struct {
pricefeedKeeper pricefeed.Keeper
bep3Keeper bep3.Keeper
kavadistKeeper kavadist.Keeper
incentiveKeeper incentive.Keeper
// the module manager
mm *module.Manager
@ -145,7 +148,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey,
gov.StoreKey, params.StoreKey, evidence.StoreKey, validatorvesting.StoreKey,
auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey, bep3.StoreKey,
kavadist.StoreKey,
kavadist.StoreKey, incentive.StoreKey,
)
tkeys := sdk.NewTransientStoreKeys(params.TStoreKey)
@ -173,6 +176,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
pricefeedSubspace := app.paramsKeeper.Subspace(pricefeed.DefaultParamspace)
bep3Subspace := app.paramsKeeper.Subspace(bep3.DefaultParamspace)
kavadistSubspace := app.paramsKeeper.Subspace(kavadist.DefaultParamspace)
incentiveSubspace := app.paramsKeeper.Subspace(incentive.DefaultParamspace)
// add keepers
app.accountKeeper = auth.NewAccountKeeper(
@ -295,6 +299,14 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
kavadistSubspace,
app.supplyKeeper,
)
app.incentiveKeeper = incentive.NewKeeper(
app.cdc,
keys[incentive.StoreKey],
incentiveSubspace,
app.supplyKeeper,
app.cdpKeeper,
app.accountKeeper,
)
// register the staking hooks
// NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks
@ -321,12 +333,13 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
pricefeed.NewAppModule(app.pricefeedKeeper, app.accountKeeper),
bep3.NewAppModule(app.bep3Keeper, app.accountKeeper, app.supplyKeeper),
kavadist.NewAppModule(app.kavadistKeeper, app.supplyKeeper),
incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper),
)
// During begin block slashing happens after distr.BeginBlocker so that
// there is nothing left over in the validator fee pool, so as to keep the
// CanWithdrawInvariant invariant.
app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName, kavadist.ModuleName, cdp.ModuleName, auction.ModuleName, bep3.ModuleName)
app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName, kavadist.ModuleName, cdp.ModuleName, auction.ModuleName, bep3.ModuleName, incentive.ModuleName)
app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName)
@ -335,7 +348,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
validatorvesting.ModuleName, distr.ModuleName,
staking.ModuleName, bank.ModuleName, slashing.ModuleName,
gov.ModuleName, mint.ModuleName, evidence.ModuleName,
pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, bep3.ModuleName, kavadist.ModuleName, // TODO is this order ok?
pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, bep3.ModuleName, kavadist.ModuleName, incentive.ModuleName,
supply.ModuleName, // calculates the total supply from account - should run after modules that modify accounts in genesis
crisis.ModuleName, // runs the invariants at genesis - should run after other modules
genutil.ModuleName, // genutils must occur after staking so that pools are properly initialized with tokens from genesis accounts.
@ -363,6 +376,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
auction.NewAppModule(app.auctionKeeper, app.accountKeeper, app.supplyKeeper),
bep3.NewAppModule(app.bep3Keeper, app.accountKeeper, app.supplyKeeper),
kavadist.NewAppModule(app.kavadistKeeper, app.supplyKeeper),
incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper),
)
app.sm.RegisterStoreDecoders()

View File

@ -8,8 +8,9 @@ const (
// Default simulation operation weights for messages and gov proposals
const (
DefaultWeightMsgPlaceBid int = 100
DefaultWeightMsgCreateAtomicSwap int = 100
DefaultWeightMsgUpdatePrices int = 100
DefaultWeightMsgPlaceBid int = 75
DefaultWeightMsgCreateAtomicSwap int = 50
DefaultWeightMsgUpdatePrices int = 50
DefaultWeightMsgCdp int = 100
DefaultWeightMsgClaimReward int = 50
)

View File

@ -6,11 +6,6 @@ import (
"os"
"testing"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/libs/log"
dbm "github.com/tendermint/tm-db"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/simapp"
"github.com/cosmos/cosmos-sdk/simapp/helpers"
@ -25,6 +20,17 @@ import (
"github.com/cosmos/cosmos-sdk/x/slashing"
"github.com/cosmos/cosmos-sdk/x/staking"
"github.com/cosmos/cosmos-sdk/x/supply"
"github.com/kava-labs/kava/x/auction"
"github.com/kava-labs/kava/x/bep3"
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/incentive"
"github.com/kava-labs/kava/x/kavadist"
"github.com/kava-labs/kava/x/pricefeed"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/libs/log"
dbm "github.com/tendermint/tm-db"
)
type StoreKeysPrefixes struct {
@ -162,6 +168,13 @@ func TestAppImportExport(t *testing.T) {
{app.keys[supply.StoreKey], newApp.keys[supply.StoreKey], [][]byte{}},
{app.keys[params.StoreKey], newApp.keys[params.StoreKey], [][]byte{}},
{app.keys[gov.StoreKey], newApp.keys[gov.StoreKey], [][]byte{}},
{app.keys[auction.StoreKey], newApp.keys[auction.StoreKey], [][]byte{}},
{app.keys[bep3.StoreKey], newApp.keys[bep3.StoreKey], [][]byte{}},
{app.keys[cdp.StoreKey], newApp.keys[cdp.StoreKey], [][]byte{}},
{app.keys[incentive.StoreKey], newApp.keys[incentive.StoreKey], [][]byte{}},
{app.keys[kavadist.StoreKey], newApp.keys[kavadist.StoreKey], [][]byte{}},
{app.keys[pricefeed.StoreKey], newApp.keys[pricefeed.StoreKey], [][]byte{}},
{app.keys[validatorvesting.StoreKey], newApp.keys[validatorvesting.StoreKey], [][]byte{}},
}
for _, skp := range storeKeysPrefixes {
@ -170,8 +183,9 @@ func TestAppImportExport(t *testing.T) {
failedKVAs, failedKVBs := sdk.DiffKVStores(storeA, storeB, skp.Prefixes)
require.Equal(t, len(failedKVAs), len(failedKVBs), "unequal sets of key-values to compare")
fmt.Printf("compared %d key/value pairs between %s and %s\n", len(failedKVAs), skp.A, skp.B)
if len(failedKVAs) != 0 {
fmt.Printf("found %d non-equal key/value pairs between %s and %s\n", len(failedKVAs), skp.A, skp.B)
}
require.Equal(t, len(failedKVAs), 0, simapp.GetSimulationLog(skp.A.Name(), app.SimulationManager().StoreDecoders, app.Codec(), failedKVAs, failedKVBs))
}
}

View File

@ -30,6 +30,7 @@ import (
"github.com/kava-labs/kava/x/auction"
"github.com/kava-labs/kava/x/bep3"
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/incentive"
"github.com/kava-labs/kava/x/kavadist"
"github.com/kava-labs/kava/x/pricefeed"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
@ -75,6 +76,7 @@ func (tApp TestApp) GetCDPKeeper() cdp.Keeper { return tApp.cdpKeepe
func (tApp TestApp) GetPriceFeedKeeper() pricefeed.Keeper { return tApp.pricefeedKeeper }
func (tApp TestApp) GetBep3Keeper() bep3.Keeper { return tApp.bep3Keeper }
func (tApp TestApp) GetKavadistKeeper() kavadist.Keeper { return tApp.kavadistKeeper }
func (tApp TestApp) GetIncentiveKeeper() incentive.Keeper { return tApp.incentiveKeeper }
// This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states
func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) TestApp {

1
go.sum
View File

@ -416,6 +416,7 @@ github.com/tendermint/iavl v0.13.2/go.mod h1:vE1u0XAGXYjHykd4BLp8p/yivrw2PF1Tuol
github.com/tendermint/tendermint v0.33.2/go.mod h1:25DqB7YvV1tN3tHsjWoc2vFtlwICfrub9XO6UBO+4xk=
github.com/tendermint/tendermint v0.33.3 h1:6lMqjEoCGejCzAghbvfQgmw87snGSqEhDTo/jw+W8CI=
github.com/tendermint/tendermint v0.33.3/go.mod h1:25DqB7YvV1tN3tHsjWoc2vFtlwICfrub9XO6UBO+4xk=
github.com/tendermint/tendermint v0.33.4 h1:NM3G9618yC5PaaxGrcAySc5ylc1PAANeIx42u2Re/jo=
github.com/tendermint/tm-db v0.4.1/go.mod h1:JsJ6qzYkCGiGwm5GHl/H5GLI9XLb6qZX7PRe425dHAY=
github.com/tendermint/tm-db v0.5.0 h1:qtM5UTr1dlRnHtDY6y7MZO5Di8XAE2j3lc/pCnKJ5hQ=
github.com/tendermint/tm-db v0.5.0/go.mod h1:lSq7q5WRR/njf1LnhiZ/lIJHk2S8Y1Zyq5oP/3o9C2U=

View File

@ -126,8 +126,8 @@ func sendBtcCdp() {
// sender, collateral, principal
msg := cdp.NewMsgCreateCDP(
addr,
sdk.NewCoins(sdk.NewInt64Coin("btc", 200000000)),
sdk.NewCoins(sdk.NewInt64Coin("usdx", 10000000)),
sdk.NewInt64Coin("btc", 200000000),
sdk.NewInt64Coin("usdx", 10000000),
)
// helper methods for transactions
@ -159,8 +159,8 @@ func sendXrpCdp() {
// sender, collateral, principal
msg := cdp.NewMsgCreateCDP(
addr,
sdk.NewCoins(sdk.NewInt64Coin("xrp", 200000000)),
sdk.NewCoins(sdk.NewInt64Coin("usdx", 10000000)),
sdk.NewInt64Coin("xrp", 200000000),
sdk.NewInt64Coin("usdx", 10000000),
)
// helper methods for transactions

View File

@ -37,7 +37,6 @@ func queryAtomicSwapHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
swapID, err := types.HexToBytes(vars[restSwapID])
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())

View File

@ -56,8 +56,7 @@ func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k Keeper) {
}
distTimeElapsed := sdk.NewInt(ctx.BlockTime().Unix() - previousDistTime.Unix())
if distTimeElapsed.GTE(sdk.NewInt(int64(params.SavingsDistributionFrequency.Seconds()))) {
for _, dp := range params.DebtParams {
err := k.DistributeSavingsRate(ctx, dp.Denom)
err := k.DistributeSavingsRate(ctx, params.DebtParam.Denom)
if err != nil {
ctx.EventManager().EmitEvent(
sdk.NewEvent(
@ -67,7 +66,6 @@ func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k Keeper) {
),
)
}
}
k.SetPreviousSavingsDistribution(ctx, ctx.BlockTime())
}
return

View File

@ -100,7 +100,7 @@ func (suite *ModuleTestSuite) createCdps() {
tracker.debt += int64(debt)
}
}
suite.Nil(suite.keeper.AddCdp(suite.ctx, addrs[j], cs(c(collateral, int64(amount))), cs(c("usdx", int64(debt)))))
suite.Nil(suite.keeper.AddCdp(suite.ctx, addrs[j], c(collateral, int64(amount)), c("usdx", int64(debt))))
c, f := suite.keeper.GetCDP(suite.ctx, collateral, uint64(j+1))
suite.True(f)
cdps[j] = c
@ -150,7 +150,7 @@ func (suite *ModuleTestSuite) TestBeginBlock() {
}
func (suite *ModuleTestSuite) TestSeizeSingleCdpWithFees() {
err := suite.keeper.AddCdp(suite.ctx, suite.addrs[0], cs(c("xrp", 10000000000)), cs(c("usdx", 1000000000)))
err := suite.keeper.AddCdp(suite.ctx, suite.addrs[0], c("xrp", 10000000000), c("usdx", 1000000000))
suite.NoError(err)
suite.Equal(i(1000000000), suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx"))
sk := suite.app.GetSupplyKeeper()

View File

@ -50,22 +50,6 @@ var (
NewAugmentedCDP = types.NewAugmentedCDP
RegisterCodec = types.RegisterCodec
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
ErrInvalidDeposit = types.ErrInvalidDeposit
ErrInvalidPayment = types.ErrInvalidPayment
ErrDepositNotAvailable = types.ErrDepositNotAvailable
ErrInvalidCollateral = types.ErrInvalidCollateral
ErrInvalidWithdrawAmount = types.ErrInvalidWithdrawAmount
ErrCdpNotAvailable = types.ErrCdpNotAvailable
ErrBelowDebtFloor = types.ErrBelowDebtFloor
ErrLoadingAugmentedCDP = types.ErrLoadingAugmentedCDP
NewGenesisState = types.NewGenesisState
DefaultGenesisState = types.DefaultGenesisState
GetCdpIDBytes = types.GetCdpIDBytes
@ -102,6 +86,23 @@ var (
// variable aliases
ModuleCdc = types.ModuleCdc
ErrCdpAlreadyExists = types.ErrCdpAlreadyExists
ErrInvalidCollateralLength = types.ErrInvalidCollateralLength
ErrCollateralNotSupported = types.ErrCollateralNotSupported
ErrDebtNotSupported = types.ErrDebtNotSupported
ErrExceedsDebtLimit = types.ErrExceedsDebtLimit
ErrInvalidCollateralRatio = types.ErrInvalidCollateralRatio
ErrCdpNotFound = types.ErrCdpNotFound
ErrDepositNotFound = types.ErrDepositNotFound
ErrInvalidDeposit = types.ErrInvalidDeposit
ErrInvalidPayment = types.ErrInvalidPayment
ErrDepositNotAvailable = types.ErrDepositNotAvailable
ErrInvalidWithdrawAmount = types.ErrInvalidWithdrawAmount
ErrCdpNotAvailable = types.ErrCdpNotAvailable
ErrBelowDebtFloor = types.ErrBelowDebtFloor
ErrLoadingAugmentedCDP = types.ErrLoadingAugmentedCDP
ErrInvalidDebtRequest = types.ErrInvalidDebtRequest
ErrDenomPrefixNotFound = types.ErrDenomPrefixNotFound
CdpIDKeyPrefix = types.CdpIDKeyPrefix
CdpKeyPrefix = types.CdpKeyPrefix
CollateralRatioIndexPrefix = types.CollateralRatioIndexPrefix
@ -113,7 +114,7 @@ var (
PreviousDistributionTimeKey = types.PreviousDistributionTimeKey
KeyGlobalDebtLimit = types.KeyGlobalDebtLimit
KeyCollateralParams = types.KeyCollateralParams
KeyDebtParams = types.KeyDebtParams
KeyDebtParam = types.KeyDebtParam
KeyDistributionFrequency = types.KeyDistributionFrequency
KeyCircuitBreaker = types.KeyCircuitBreaker
KeyDebtThreshold = types.KeyDebtThreshold
@ -121,10 +122,11 @@ var (
DefaultGlobalDebt = types.DefaultGlobalDebt
DefaultCircuitBreaker = types.DefaultCircuitBreaker
DefaultCollateralParams = types.DefaultCollateralParams
DefaultDebtParams = types.DefaultDebtParams
DefaultDebtParam = types.DefaultDebtParam
DefaultCdpStartingID = types.DefaultCdpStartingID
DefaultDebtDenom = types.DefaultDebtDenom
DefaultGovDenom = types.DefaultGovDenom
DefaultStableDenom = types.DefaultStableDenom
DefaultSurplusThreshold = types.DefaultSurplusThreshold
DefaultDebtThreshold = types.DefaultDebtThreshold
DefaultPreviousDistributionTime = types.DefaultPreviousDistributionTime
@ -140,8 +142,6 @@ type (
AugmentedCDPs = types.AugmentedCDPs
Deposit = types.Deposit
Deposits = types.Deposits
SupplyKeeper = types.SupplyKeeper
PricefeedKeeper = types.PricefeedKeeper
GenesisState = types.GenesisState
MsgCreateCDP = types.MsgCreateCDP
MsgDeposit = types.MsgDeposit

View File

@ -53,11 +53,11 @@ $ %s tx %s create 10000000uatom 1000usdx --from myKeyName
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
collateral, err := sdk.ParseCoins(args[0])
collateral, err := sdk.ParseCoin(args[0])
if err != nil {
return err
}
debt, err := sdk.ParseCoins(args[1])
debt, err := sdk.ParseCoin(args[1])
if err != nil {
return err
}
@ -88,7 +88,7 @@ $ %s tx %s deposit kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw 10000000uatom --f
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
collateral, err := sdk.ParseCoins(args[1])
collateral, err := sdk.ParseCoin(args[1])
if err != nil {
return err
}
@ -123,7 +123,7 @@ $ %s tx %s withdraw kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw 10000000uatom --
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
collateral, err := sdk.ParseCoins(args[1])
collateral, err := sdk.ParseCoin(args[1])
if err != nil {
return err
}
@ -158,7 +158,7 @@ $ %s tx %s draw uatom 1000usdx --from myKeyName
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
debt, err := sdk.ParseCoins(args[1])
debt, err := sdk.ParseCoin(args[1])
if err != nil {
return err
}
@ -189,7 +189,7 @@ $ %s tx %s repay uatom 1000usdx --from myKeyName
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
payment, err := sdk.ParseCoins(args[1])
payment, err := sdk.ParseCoin(args[1])
if err != nil {
return err
}

View File

@ -18,8 +18,8 @@ func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
type PostCdpReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
Collateral sdk.Coins `json:"collateral" yaml:"collateral"`
Principal sdk.Coins `json:"principal" yaml:"principal"`
Collateral sdk.Coin `json:"collateral" yaml:"collateral"`
Principal sdk.Coin `json:"principal" yaml:"principal"`
}
// PostDepositReq defines the properties of cdp request's body.
@ -27,7 +27,7 @@ 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"`
Collateral sdk.Coin `json:"collateral" yaml:"collateral"`
}
// PostWithdrawalReq defines the properties of cdp request's body.
@ -35,7 +35,7 @@ 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"`
Collateral sdk.Coin `json:"collateral" yaml:"collateral"`
}
// PostDrawReq defines the properties of cdp request's body.
@ -43,7 +43,7 @@ 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"`
Principal sdk.Coin `json:"principal" yaml:"principal"`
}
// PostRepayReq defines the properties of cdp request's body.
@ -51,5 +51,5 @@ 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:"payment" yaml:"payment"`
Payment sdk.Coin `json:"payment" yaml:"payment"`
}

View File

@ -4,10 +4,11 @@ import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/cdp/types"
)
// InitGenesis sets initial genesis state for cdp module
func InitGenesis(ctx sdk.Context, k Keeper, pk PricefeedKeeper, sk SupplyKeeper, gs GenesisState) {
func InitGenesis(ctx sdk.Context, k Keeper, pk types.PricefeedKeeper, sk types.SupplyKeeper, gs GenesisState) {
if err := gs.Validate(); err != nil {
panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err))
@ -46,9 +47,7 @@ func InitGenesis(ctx sdk.Context, k Keeper, pk PricefeedKeeper, sk SupplyKeeper,
// 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.SetTotalPrincipal(ctx, cp.Denom, gs.Params.DebtParam.Denom, sdk.ZeroInt())
}
// add cdps
@ -56,16 +55,20 @@ func InitGenesis(ctx sdk.Context, k Keeper, pk PricefeedKeeper, sk SupplyKeeper,
if cdp.ID == gs.StartingCdpID {
panic(fmt.Sprintf("starting cdp id is assigned to an existing cdp: %s", cdp))
}
k.SetCDP(ctx, cdp)
err := k.SetCDP(ctx, cdp)
if err != nil {
panic(fmt.Sprintf("error setting cdp: %v", err))
}
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)
ratio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
k.IndexCdpByCollateralRatio(ctx, cdp.Collateral.Denom, cdp.ID, ratio)
k.IncrementTotalPrincipal(ctx, cdp.Collateral.Denom, cdp.Principal.Add(cdp.AccumulatedFees))
}
k.SetNextCdpID(ctx, gs.StartingCdpID)
k.SetDebtDenom(ctx, gs.DebtDenom)
k.SetGovDenom(ctx, gs.GovDenom)
k.SetPreviousSavingsDistribution(ctx, gs.PreviousDistributionTime)
for _, d := range gs.Deposits {
k.SetDeposit(ctx, d)

View File

@ -1,12 +1,13 @@
package cdp_test
import (
"strings"
"testing"
"time"
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"
)
@ -18,16 +19,86 @@ type GenesisTestSuite struct {
}
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)
type args struct {
params cdp.Params
cdps cdp.CDPs
deposits cdp.Deposits
startingID uint64
debtDenom string
govDenom string
prevDistTime time.Time
}
type errArgs struct {
expectPass bool
contains string
}
type genesisTest struct {
name string
args args
errArgs errArgs
}
testCases := []struct {
name string
args args
errArgs errArgs
}{
{
name: "empty debt denom",
args: args{
params: cdp.DefaultParams(),
cdps: cdp.CDPs{},
deposits: cdp.Deposits{},
debtDenom: "",
govDenom: cdp.DefaultGovDenom,
prevDistTime: cdp.DefaultPreviousDistributionTime,
},
errArgs: errArgs{
expectPass: false,
contains: "debt denom invalid",
},
},
{
name: "empty gov denom",
args: args{
params: cdp.DefaultParams(),
cdps: cdp.CDPs{},
deposits: cdp.Deposits{},
debtDenom: cdp.DefaultDebtDenom,
govDenom: "",
prevDistTime: cdp.DefaultPreviousDistributionTime,
},
errArgs: errArgs{
expectPass: false,
contains: "gov denom invalid",
},
},
{
name: "empty distribution time",
args: args{
params: cdp.DefaultParams(),
cdps: cdp.CDPs{},
deposits: cdp.Deposits{},
debtDenom: cdp.DefaultDebtDenom,
govDenom: cdp.DefaultGovDenom,
prevDistTime: time.Time{},
},
errArgs: errArgs{
expectPass: false,
contains: "previous distribution time not set",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
gs := cdp.NewGenesisState(tc.args.params, tc.args.cdps, tc.args.deposits, tc.args.startingID, tc.args.debtDenom, tc.args.govDenom, tc.args.prevDistTime)
err := gs.Validate()
if tc.errArgs.expectPass {
suite.Require().NoError(err)
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}

View File

@ -38,7 +38,7 @@ func handleMsgCreateCDP(ctx sdk.Context, k Keeper, msg MsgCreateCDP) (*sdk.Resul
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()),
),
)
id, _ := k.GetCdpID(ctx, msg.Sender, msg.Collateral[0].Denom)
id, _ := k.GetCdpID(ctx, msg.Sender, msg.Collateral.Denom)
return &sdk.Result{
Data: GetCdpIDBytes(id),

View File

@ -43,8 +43,8 @@ func (suite *HandlerTestSuite) TestMsgCreateCdp() {
ak.SetAccount(suite.ctx, acc)
msg := cdp.NewMsgCreateCDP(
addrs[0],
cs(c("xrp", 200000000)),
cs(c("usdx", 10000000)),
c("xrp", 200000000),
c("usdx", 10000000),
)
res, err := suite.handler(suite.ctx, msg)
suite.Require().NoError(err)

View File

@ -39,7 +39,7 @@ func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState {
func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
cdpGenesis := cdp.GenesisState{
Params: cdp.Params{
GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)),
GlobalDebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
DebtAuctionThreshold: cdp.DefaultDebtThreshold,
SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
@ -47,7 +47,7 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
{
Denom: asset,
LiquidationRatio: liquidationRatio,
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)),
DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr
LiquidationPenalty: d("0.05"),
AuctionSize: i(1000000000),
@ -56,8 +56,7 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
MarketID: asset + ":usd",
},
},
DebtParams: cdp.DebtParams{
{
DebtParam: cdp.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: i(6),
@ -65,7 +64,6 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
SavingsRate: d("0.95"),
},
},
},
StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom,
GovDenom: cdp.DefaultGovDenom,
@ -103,7 +101,7 @@ func NewPricefeedGenStateMulti() app.GenesisState {
func NewCDPGenStateMulti() app.GenesisState {
cdpGenesis := cdp.GenesisState{
Params: cdp.Params{
GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)),
GlobalDebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
DebtAuctionThreshold: cdp.DefaultDebtThreshold,
SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
@ -111,7 +109,7 @@ func NewCDPGenStateMulti() app.GenesisState {
{
Denom: "xrp",
LiquidationRatio: sdk.MustNewDecFromStr("2.0"),
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)),
DebtLimit: sdk.NewInt64Coin("usdx", 500000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr
LiquidationPenalty: d("0.05"),
AuctionSize: i(7000000000),
@ -122,7 +120,7 @@ func NewCDPGenStateMulti() app.GenesisState {
{
Denom: "btc",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)),
DebtLimit: sdk.NewInt64Coin("usdx", 500000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr
LiquidationPenalty: d("0.025"),
AuctionSize: i(10000000),
@ -131,22 +129,13 @@ func NewCDPGenStateMulti() app.GenesisState {
ConversionFactor: i(8),
},
},
DebtParams: cdp.DebtParams{
{
DebtParam: cdp.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
SavingsRate: d("0.95"),
},
{
Denom: "susd",
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
SavingsRate: d("0.95"),
},
},
},
StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom,
@ -159,121 +148,10 @@ func NewCDPGenStateMulti() app.GenesisState {
func cdps() (cdps cdp.CDPs) {
_, addrs := app.GeneratePrivKeyAddressPairs(3)
c1 := cdp.NewCDP(uint64(1), addrs[0], sdk.NewCoins(sdk.NewCoin("xrp", sdk.NewInt(100000000))), 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(50000000))), tmtime.Canonical(time.Now()))
c1 := cdp.NewCDP(uint64(1), addrs[0], sdk.NewCoin("xrp", sdk.NewInt(100000000)), sdk.NewCoin("usdx", sdk.NewInt(8000000)), tmtime.Canonical(time.Now()))
c2 := cdp.NewCDP(uint64(2), addrs[1], sdk.NewCoin("xrp", sdk.NewInt(100000000)), sdk.NewCoin("usdx", sdk.NewInt(10000000)), tmtime.Canonical(time.Now()))
c3 := cdp.NewCDP(uint64(3), addrs[1], sdk.NewCoin("btc", sdk.NewInt(1000000000)), sdk.NewCoin("usdx", sdk.NewInt(10000000)), tmtime.Canonical(time.Now()))
c4 := cdp.NewCDP(uint64(4), addrs[2], sdk.NewCoin("xrp", sdk.NewInt(1000000000)), sdk.NewCoin("usdx", sdk.NewInt(50000000)), 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"
g8 := baseGenState()
g8.Params.DebtParams = append(g8.Params.DebtParams, cdp.DebtParam{
Denom: "lol",
ReferenceAsset: "usd",
})
g9 := baseGenState()
g9.DebtDenom = ""
g10 := baseGenState()
g10.Params.CollateralParams[0].AuctionSize = i(-10)
g11 := baseGenState()
g11.Params.CollateralParams[0].LiquidationPenalty = d("5.0")
g12 := baseGenState()
g12.GovDenom = ""
g13 := baseGenState()
g13.Params.DebtParams[0].SavingsRate = d("4.0")
return []badGenState{
badGenState{Genesis: g1, Reason: "duplicate collateral denom"},
badGenState{Genesis: g2, Reason: "duplicate collateral prefix"},
badGenState{Genesis: g3, Reason: "invalid debt limit"},
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: g8, Reason: "debt param not found in global debt limit"},
badGenState{Genesis: g9, Reason: "debt denom not set"},
badGenState{Genesis: g10, Reason: "negative auction size"},
badGenState{Genesis: g11, Reason: "invalid liquidation penalty"},
badGenState{Genesis: g12, Reason: "gov denom not set"},
badGenState{Genesis: g13, Reason: "invalid savings rate"},
}
}
func baseGenState() cdp.GenesisState {
return cdp.GenesisState{
Params: cdp.Params{
GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)),
SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
DebtAuctionThreshold: cdp.DefaultDebtThreshold,
SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
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",
ConversionFactor: i(6),
DebtFloor: i(10000000),
},
{
Denom: "susd",
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
},
},
},
StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom,
GovDenom: cdp.DefaultGovDenom,
CDPs: cdp.CDPs{},
PreviousDistributionTime: cdp.DefaultPreviousDistributionTime,
}
}

View File

@ -12,11 +12,11 @@ const (
type partialDeposit struct {
Depositor sdk.AccAddress
Amount sdk.Coins
Amount sdk.Coin
DebtShare sdk.Int
}
func newPartialDeposit(depositor sdk.AccAddress, amount sdk.Coins, ds sdk.Int) partialDeposit {
func newPartialDeposit(depositor sdk.AccAddress, amount sdk.Coin, ds sdk.Int) partialDeposit {
return partialDeposit{
Depositor: depositor,
Amount: amount,
@ -29,7 +29,7 @@ type partialDeposits []partialDeposit
func (pd partialDeposits) SumCollateral() (sum sdk.Int) {
sum = sdk.ZeroInt()
for _, d := range pd {
sum = sum.Add(d.Amount[0].Amount)
sum = sum.Add(d.Amount.Amount)
}
return
}
@ -44,7 +44,7 @@ func (pd partialDeposits) SumDebt() (sum sdk.Int) {
// AuctionCollateral creates auctions from the input deposits which attempt to raise the corresponding amount of debt
func (k Keeper) AuctionCollateral(ctx sdk.Context, deposits types.Deposits, debt sdk.Int, bidDenom string) error {
auctionSize := k.getAuctionSize(ctx, deposits[0].Amount[0].Denom)
auctionSize := k.getAuctionSize(ctx, deposits[0].Amount.Denom)
partialAuctionDeposits := partialDeposits{}
totalCollateral := deposits.SumCollateral()
for totalCollateral.GT(sdk.ZeroInt()) {
@ -52,8 +52,8 @@ func (k Keeper) AuctionCollateral(ctx sdk.Context, deposits types.Deposits, debt
if dep.Amount.IsZero() {
continue
}
collateralAmount := dep.Amount[0].Amount
collateralDenom := dep.Amount[0].Denom
collateralAmount := dep.Amount.Amount
collateralDenom := dep.Amount.Denom
// create auctions from individual deposits that are larger than the auction size
debtChange, collateralChange, err := k.CreateAuctionsFromDeposit(ctx, dep, debt, totalCollateral, auctionSize, bidDenom)
if err != nil {
@ -61,7 +61,7 @@ func (k Keeper) AuctionCollateral(ctx sdk.Context, deposits types.Deposits, debt
}
debt = debt.Sub(debtChange)
totalCollateral = totalCollateral.Sub(collateralChange)
dep.Amount = sdk.NewCoins(sdk.NewCoin(collateralDenom, collateralAmount.Sub(collateralChange)))
dep.Amount = sdk.NewCoin(collateralDenom, collateralAmount.Sub(collateralChange))
collateralAmount = collateralAmount.Sub(collateralChange)
// if there is leftover collateral that is less than a lot
if !dep.Amount.IsZero() {
@ -73,11 +73,11 @@ func (k Keeper) AuctionCollateral(ctx sdk.Context, deposits types.Deposits, debt
// append the deposit to the partial deposits and zero out the deposit
pd := newPartialDeposit(dep.Depositor, dep.Amount, debtCoveredByDeposit)
partialAuctionDeposits = append(partialAuctionDeposits, pd)
dep.Amount = sdk.NewCoins(sdk.NewCoin(collateralDenom, sdk.ZeroInt()))
dep.Amount = sdk.NewCoin(collateralDenom, sdk.ZeroInt())
} else {
// if the sum of partial deposits now makes a lot
partialCollateral := sdk.NewCoins(sdk.NewCoin(collateralDenom, auctionSize.Sub(partialAuctionDeposits.SumCollateral())))
partialAmount := partialCollateral[0].Amount
partialCollateral := sdk.NewCoin(collateralDenom, auctionSize.Sub(partialAuctionDeposits.SumCollateral()))
partialAmount := partialCollateral.Amount
partialDebt := (partialAmount.Quo(collateralAmount)).Mul(debtCoveredByDeposit)
// create a partial deposit from the deposit
@ -93,7 +93,7 @@ func (k Keeper) AuctionCollateral(ctx sdk.Context, deposits types.Deposits, debt
totalCollateral = totalCollateral.Sub(collateralChange)
// reset partial deposits and update the deposit amount
partialAuctionDeposits = partialDeposits{}
dep.Amount = sdk.NewCoins(sdk.NewCoin(collateralDenom, collateralAmount.Sub(partialAmount)))
dep.Amount = sdk.NewCoin(collateralDenom, collateralAmount.Sub(partialAmount))
}
}
deposits[i] = dep
@ -110,11 +110,13 @@ func (k Keeper) AuctionCollateral(ctx sdk.Context, deposits types.Deposits, debt
}
// CreateAuctionsFromDeposit creates auctions from the input deposit until there is less than auctionSize left on the deposit
func (k Keeper) CreateAuctionsFromDeposit(ctx sdk.Context, dep types.Deposit, debt sdk.Int, totalCollateral sdk.Int, auctionSize sdk.Int, principalDenom string) (debtChange sdk.Int, collateralChange sdk.Int, err error) {
func (k Keeper) CreateAuctionsFromDeposit(
ctx sdk.Context, dep types.Deposit, debt sdk.Int, totalCollateral sdk.Int, auctionSize sdk.Int,
principalDenom string) (debtChange sdk.Int, collateralChange sdk.Int, err error) {
debtChange = sdk.ZeroInt()
collateralChange = sdk.ZeroInt()
depositAmount := dep.Amount[0].Amount
depositDenom := dep.Amount[0].Denom
depositAmount := dep.Amount.Amount
depositDenom := dep.Amount.Denom
for depositAmount.GTE(auctionSize) {
// figure out how much debt is covered by one lots worth of collateral
depositDebtAmount := (sdk.NewDecFromInt(auctionSize).Quo(sdk.NewDecFromInt(totalCollateral))).Mul(sdk.NewDecFromInt(debt)).RoundInt()
@ -142,13 +144,13 @@ func (k Keeper) CreateAuctionFromPartialDeposits(ctx sdk.Context, partialDeps pa
returnAddrs := []sdk.AccAddress{}
returnWeights := []sdk.Int{}
depositDenom := partialDeps[0].Amount[0].Denom
depositDenom := partialDeps[0].Amount.Denom
for _, pd := range partialDeps {
returnAddrs = append(returnAddrs, pd.Depositor)
returnWeights = append(returnWeights, pd.DebtShare)
}
penalty := k.ApplyLiquidationPenalty(ctx, depositDenom, partialDeps.SumDebt())
_, err = k.auctionKeeper.StartCollateralAuction(ctx, types.LiquidatorMacc, sdk.NewCoin(partialDeps[0].Amount[0].Denom, auctionSize), sdk.NewCoin(bidDenom, partialDeps.SumDebt().Add(penalty)), returnAddrs, returnWeights, sdk.NewCoin(k.GetDebtDenom(ctx), partialDeps.SumDebt()))
_, err = k.auctionKeeper.StartCollateralAuction(ctx, types.LiquidatorMacc, sdk.NewCoin(partialDeps[0].Amount.Denom, auctionSize), sdk.NewCoin(bidDenom, partialDeps.SumDebt().Add(penalty)), returnAddrs, returnWeights, sdk.NewCoin(k.GetDebtDenom(ctx), partialDeps.SumDebt()))
if err != nil {
return sdk.ZeroInt(), sdk.ZeroInt(), err
}
@ -166,28 +168,25 @@ func (k Keeper) NetSurplusAndDebt(ctx sdk.Context) error {
if netAmount.IsZero() {
return nil
}
// burn debt coins equal to netAmount
err := k.supplyKeeper.BurnCoins(ctx, types.LiquidatorMacc, sdk.NewCoins(sdk.NewCoin(k.GetDebtDenom(ctx), netAmount)))
if err != nil {
return err
}
for netAmount.GT(sdk.ZeroInt()) {
for _, dp := range k.GetParams(ctx).DebtParams {
// burn stable coins equal to netAmount
dp := k.GetParams(ctx).DebtParam
balance := k.supplyKeeper.GetModuleAccount(ctx, types.LiquidatorMacc).GetCoins().AmountOf(dp.Denom)
if balance.LT(netAmount) {
err = k.supplyKeeper.BurnCoins(ctx, types.LiquidatorMacc, sdk.NewCoins(sdk.NewCoin(dp.Denom, balance)))
if err != nil {
return err
}
netAmount = netAmount.Sub(balance)
} else {
return nil
}
err = k.supplyKeeper.BurnCoins(ctx, types.LiquidatorMacc, sdk.NewCoins(sdk.NewCoin(dp.Denom, netAmount)))
if err != nil {
return err
}
netAmount = sdk.ZeroInt()
}
}
}
return nil
}
@ -195,10 +194,9 @@ func (k Keeper) NetSurplusAndDebt(ctx sdk.Context) error {
func (k Keeper) GetTotalSurplus(ctx sdk.Context, accountName string) sdk.Int {
acc := k.supplyKeeper.GetModuleAccount(ctx, accountName)
totalSurplus := sdk.ZeroInt()
for _, dp := range k.GetParams(ctx).DebtParams {
dp := k.GetParams(ctx).DebtParam
surplus := acc.GetCoins().AmountOf(dp.Denom)
totalSurplus = totalSurplus.Add(surplus)
}
return totalSurplus
}
@ -221,15 +219,13 @@ func (k Keeper) RunSurplusAndDebtAuctions(ctx sdk.Context) error {
}
}
for _, dp := range params.DebtParams {
surplus := k.supplyKeeper.GetModuleAccount(ctx, types.LiquidatorMacc).GetCoins().AmountOf(dp.Denom)
surplus := k.supplyKeeper.GetModuleAccount(ctx, types.LiquidatorMacc).GetCoins().AmountOf(params.DebtParam.Denom)
if surplus.GTE(params.SurplusAuctionThreshold) {
surplusLot := sdk.NewCoin(dp.Denom, surplus)
surplusLot := sdk.NewCoin(params.DebtParam.Denom, surplus)
_, err := k.auctionKeeper.StartSurplusAuction(ctx, types.LiquidatorMacc, surplusLot, k.GetGovDenom(ctx))
if err != nil {
return err
}
}
}
return nil
}

View File

@ -1,8 +1,8 @@
package keeper
import (
"bytes"
"fmt"
"sort"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
@ -14,26 +14,26 @@ import (
const BaseDigitFactor = 1000000000000000000
// 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) error {
func (k Keeper) AddCdp(ctx sdk.Context, owner sdk.AccAddress, collateral sdk.Coin, principal sdk.Coin) error {
// validation
err := k.ValidateCollateral(ctx, collateral)
if err != nil {
return err
}
_, found := k.GetCdpByOwnerAndDenom(ctx, owner, collateral[0].Denom)
_, found := k.GetCdpByOwnerAndDenom(ctx, owner, collateral.Denom)
if found {
return sdkerrors.Wrapf(types.ErrCdpAlreadyExists, "owner %s, denom %s", owner, collateral[0].Denom)
return sdkerrors.Wrapf(types.ErrCdpAlreadyExists, "owner %s, denom %s", owner, collateral.Denom)
}
err = k.ValidatePrincipalAdd(ctx, principal)
if err != nil {
return err
}
err = k.ValidateDebtLimit(ctx, collateral[0].Denom, principal)
err = k.ValidateDebtLimit(ctx, collateral.Denom, principal)
if err != nil {
return err
}
err = k.ValidateCollateralizationRatio(ctx, collateral, principal, sdk.NewCoins())
err = k.ValidateCollateralizationRatio(ctx, collateral, principal, sdk.NewCoin(principal.Denom, sdk.ZeroInt()))
if err != nil {
return err
}
@ -42,17 +42,17 @@ func (k Keeper) AddCdp(ctx sdk.Context, owner sdk.AccAddress, collateral sdk.Coi
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)
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, sdk.NewCoins(collateral))
if err != nil {
return err
}
// mint the principal and send to the owners account
err = k.supplyKeeper.MintCoins(ctx, types.ModuleName, principal)
err = k.supplyKeeper.MintCoins(ctx, types.ModuleName, sdk.NewCoins(principal))
if err != nil {
panic(err)
}
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, owner, principal)
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, owner, sdk.NewCoins(principal))
if err != nil {
panic(err)
}
@ -86,11 +86,14 @@ func (k Keeper) AddCdp(ctx sdk.Context, owner sdk.AccAddress, collateral sdk.Coi
)
// update total principal for input collateral type
k.IncrementTotalPrincipal(ctx, collateral[0].Denom, principal)
k.IncrementTotalPrincipal(ctx, collateral.Denom, principal)
// set the cdp, deposit, and indexes in the store
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, collateral, principal)
k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
err = k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
if err != nil {
return err
}
k.IndexCdpByOwner(ctx, cdp)
k.SetDeposit(ctx, deposit)
k.SetNextCdpID(ctx, id+1)
@ -98,18 +101,19 @@ func (k Keeper) AddCdp(ctx sdk.Context, owner sdk.AccAddress, collateral sdk.Coi
}
// 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)
func (k Keeper) SetCdpAndCollateralRatioIndex(ctx sdk.Context, cdp types.CDP, ratio sdk.Dec) error {
err := k.SetCDP(ctx, cdp)
if err != nil {
return err
}
k.IndexCdpByCollateralRatio(ctx, cdp.Collateral.Denom, cdp.ID, ratio)
return nil
}
// MintDebtCoins mints debt coins in the cdp module account
func (k Keeper) MintDebtCoins(ctx sdk.Context, moduleAccount string, denom string, principalCoins sdk.Coins) error {
coinsToMint := sdk.NewCoins()
for _, sc := range principalCoins {
coinsToMint = coinsToMint.Add(sdk.NewCoin(denom, sc.Amount))
}
err := k.supplyKeeper.MintCoins(ctx, moduleAccount, coinsToMint)
func (k Keeper) MintDebtCoins(ctx sdk.Context, moduleAccount string, denom string, principalCoins sdk.Coin) error {
debtCoins := sdk.NewCoins(sdk.NewCoin(denom, principalCoins.Amount))
err := k.supplyKeeper.MintCoins(ctx, moduleAccount, debtCoins)
if err != nil {
return err
}
@ -117,12 +121,9 @@ func (k Keeper) MintDebtCoins(ctx sdk.Context, moduleAccount string, denom strin
}
// BurnDebtCoins burns debt coins from the cdp module account
func (k Keeper) BurnDebtCoins(ctx sdk.Context, moduleAccount string, denom string, paymentCoins sdk.Coins) error {
coinsToBurn := sdk.NewCoins()
for _, pc := range paymentCoins {
coinsToBurn = coinsToBurn.Add(sdk.NewCoin(denom, pc.Amount))
}
err := k.supplyKeeper.BurnCoins(ctx, moduleAccount, coinsToBurn)
func (k Keeper) BurnDebtCoins(ctx sdk.Context, moduleAccount string, denom string, paymentCoins sdk.Coin) error {
debtCoins := sdk.NewCoins(sdk.NewCoin(denom, paymentCoins.Amount))
err := k.supplyKeeper.BurnCoins(ctx, moduleAccount, debtCoins)
if err != nil {
return err
}
@ -150,8 +151,7 @@ func (k Keeper) GetCdpID(ctx sdk.Context, owner sdk.AccAddress, denom string) (u
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}) {
if bz == nil {
return []uint64{}, false
}
var cdpIDs []uint64
@ -178,7 +178,10 @@ func (k Keeper) GetCdpByOwnerAndDenom(ctx sdk.Context, owner sdk.AccAddress, den
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)
db, found := k.GetDenomPrefix(ctx, collateralDenom)
if !found {
return types.CDP{}, false
}
// get CDP
bz := store.Get(types.CdpKey(db, cdpID))
// unmarshal
@ -191,19 +194,26 @@ func (k Keeper) GetCDP(ctx sdk.Context, collateralDenom string, cdpID uint64) (t
}
// SetCDP sets a cdp in the store
func (k Keeper) SetCDP(ctx sdk.Context, cdp types.CDP) {
func (k Keeper) SetCDP(ctx sdk.Context, cdp types.CDP) error {
store := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix)
db, _ := k.GetDenomPrefix(ctx, cdp.Collateral[0].Denom)
db, found := k.GetDenomPrefix(ctx, cdp.Collateral.Denom)
if !found {
sdkerrors.Wrapf(types.ErrDenomPrefixNotFound, "%s", cdp.Collateral.Denom)
}
bz := k.cdc.MustMarshalBinaryLengthPrefixed(cdp)
store.Set(types.CdpKey(db, cdp.ID), bz)
return
return nil
}
// DeleteCDP deletes a cdp from the store
func (k Keeper) DeleteCDP(ctx sdk.Context, cdp types.CDP) {
func (k Keeper) DeleteCDP(ctx sdk.Context, cdp types.CDP) error {
store := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix)
db, _ := k.GetDenomPrefix(ctx, cdp.Collateral[0].Denom)
db, found := k.GetDenomPrefix(ctx, cdp.Collateral.Denom)
if !found {
sdkerrors.Wrapf(types.ErrDenomPrefixNotFound, "%s", cdp.Collateral.Denom)
}
store.Delete(types.CdpKey(db, cdp.ID))
return nil
}
@ -261,14 +271,10 @@ func (k Keeper) IndexCdpByOwner(ctx sdk.Context, cdp types.CDP) {
store.Set(cdp.Owner, idBytes)
return
}
for _, id := range cdpIDs {
if id == cdp.ID {
return
}
cdpIDs = append(cdpIDs, cdp.ID)
sort.Slice(cdpIDs, func(i, j int) bool { return cdpIDs[i] < cdpIDs[j] })
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) {
@ -285,9 +291,9 @@ func (k Keeper) RemoveCdpOwnerIndex(ctx sdk.Context, cdp types.CDP) {
}
if len(updatedCdpIds) == 0 {
store.Delete(cdp.Owner)
return
}
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
@ -341,118 +347,99 @@ func (k Keeper) SetGovDenom(ctx sdk.Context, denom string) {
}
// ValidateCollateral validates that a collateral is valid for use in cdps
func (k Keeper) ValidateCollateral(ctx sdk.Context, collateral sdk.Coins) error {
if len(collateral) != 1 {
return sdkerrors.Wrapf(types.ErrInvalidCollateralLength, "%d", len(collateral))
}
_, found := k.GetCollateral(ctx, collateral[0].Denom)
func (k Keeper) ValidateCollateral(ctx sdk.Context, collateral sdk.Coin) error {
_, found := k.GetCollateral(ctx, collateral.Denom)
if !found {
return sdkerrors.Wrap(types.ErrCollateralNotSupported, collateral[0].Denom)
return sdkerrors.Wrap(types.ErrCollateralNotSupported, collateral.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) error {
for _, dc := range principal {
dp, found := k.GetDebtParam(ctx, dc.Denom)
func (k Keeper) ValidatePrincipalAdd(ctx sdk.Context, principal sdk.Coin) error {
dp, found := k.GetDebtParam(ctx, principal.Denom)
if !found {
return sdkerrors.Wrap(types.ErrDebtNotSupported, dc.Denom)
}
if dc.Amount.LT(dp.DebtFloor) {
return sdkerrors.Wrapf(types.ErrBelowDebtFloor, "proposed %s < minimum %s", dc, dp.DebtFloor)
return sdkerrors.Wrap(types.ErrDebtNotSupported, principal.Denom)
}
if principal.Amount.LT(dp.DebtFloor) {
return sdkerrors.Wrapf(types.ErrBelowDebtFloor, "proposed %s < minimum %s", principal, 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) error {
for _, dc := range principal {
_, found := k.GetDebtParam(ctx, dc.Denom)
if !found {
return sdkerrors.Wrap(types.ErrDebtNotSupported, dc.Denom)
func (k Keeper) ValidatePrincipalDraw(ctx sdk.Context, principal sdk.Coin, expectedDenom string) error {
if principal.Denom != expectedDenom {
return sdkerrors.Wrapf(types.ErrInvalidDebtRequest, "proposed %s, expected %s", principal.Denom, expectedDenom)
}
_, found := k.GetDebtParam(ctx, principal.Denom)
if !found {
return sdkerrors.Wrap(types.ErrDebtNotSupported, principal.Denom)
}
return nil
}
// ValidateDebtLimit validates that the input debt amount does not exceed the global debt limit or the debt limit for that collateral
func (k Keeper) ValidateDebtLimit(ctx sdk.Context, collateralDenom string, principal sdk.Coins) error {
func (k Keeper) ValidateDebtLimit(ctx sdk.Context, collateralDenom string, principal sdk.Coin) error {
cp, found := k.GetCollateral(ctx, collateralDenom)
if !found {
return sdkerrors.Wrap(types.ErrCollateralNotSupported, collateralDenom)
}
for _, dc := range principal {
totalPrincipal := k.GetTotalPrincipal(ctx, collateralDenom, dc.Denom).Add(dc.Amount)
collateralLimit := cp.DebtLimit.AmountOf(dc.Denom)
totalPrincipal := k.GetTotalPrincipal(ctx, collateralDenom, principal.Denom).Add(principal.Amount)
collateralLimit := cp.DebtLimit.Amount
if totalPrincipal.GT(collateralLimit) {
return sdkerrors.Wrapf(types.ErrExceedsDebtLimit, "debt increase %s > collateral debt limit %s", sdk.NewCoins(sdk.NewCoin(dc.Denom, totalPrincipal)), sdk.NewCoins(sdk.NewCoin(dc.Denom, collateralLimit)))
return sdkerrors.Wrapf(types.ErrExceedsDebtLimit, "debt increase %s > collateral debt limit %s", sdk.NewCoins(sdk.NewCoin(principal.Denom, totalPrincipal)), sdk.NewCoins(sdk.NewCoin(principal.Denom, collateralLimit)))
}
globalLimit := k.GetParams(ctx).GlobalDebtLimit.AmountOf(dc.Denom)
globalLimit := k.GetParams(ctx).GlobalDebtLimit.Amount
if totalPrincipal.GT(globalLimit) {
return sdkerrors.Wrapf(types.ErrExceedsDebtLimit, "debt increase %s > global debt limit %s", sdk.NewCoin(dc.Denom, totalPrincipal), sdk.NewCoin(dc.Denom, globalLimit))
}
return sdkerrors.Wrapf(types.ErrExceedsDebtLimit, "debt increase %s > global debt limit %s", sdk.NewCoin(principal.Denom, totalPrincipal), sdk.NewCoin(principal.Denom, globalLimit))
}
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) error {
func (k Keeper) ValidateCollateralizationRatio(ctx sdk.Context, collateral sdk.Coin, principal sdk.Coin, fees sdk.Coin) error {
//
collateralizationRatio, err := k.CalculateCollateralizationRatio(ctx, collateral, principal, fees)
if err != nil {
return err
}
liquidationRatio := k.getLiquidationRatio(ctx, collateral[0].Denom)
liquidationRatio := k.getLiquidationRatio(ctx, collateral.Denom)
if collateralizationRatio.LT(liquidationRatio) {
return sdkerrors.Wrapf(types.ErrInvalidCollateralRatio, "collateral %s, collateral ratio %s, liquidation ratio %s", collateral[0].Denom, collateralizationRatio, liquidationRatio)
return sdkerrors.Wrapf(types.ErrInvalidCollateralRatio, "collateral %s, collateral ratio %s, liquidation ratio %s", collateral.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)
}
func (k Keeper) CalculateCollateralToDebtRatio(ctx sdk.Context, collateral sdk.Coin, debt sdk.Coin) sdk.Dec {
debtTotal := k.convertDebtToBaseUnits(ctx, debt)
if debtTotal.IsZero() || debtTotal.GTE(types.MaxSortableDec) {
return types.MaxSortableDec.Sub(sdk.SmallestDec())
}
collateralBaseUnits := k.convertCollateralToBaseUnits(ctx, collateral[0])
collateralBaseUnits := k.convertCollateralToBaseUnits(ctx, collateral)
return collateralBaseUnits.Quo(debtTotal)
}
// LoadAugmentedCDP creates a new augmented CDP from an existing CDP
func (k Keeper) LoadAugmentedCDP(ctx sdk.Context, cdp types.CDP) (types.AugmentedCDP, error) {
// calculate additional 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)
totalFees := cdp.AccumulatedFees.Add(fees...)
// calculate collateralization ratio
collateralizationRatio, err := k.CalculateCollateralizationRatio(ctx, cdp.Collateral, cdp.Principal, totalFees)
collateralizationRatio, err := k.CalculateCollateralizationRatio(ctx, cdp.Collateral, cdp.Principal, cdp.AccumulatedFees)
if err != nil {
return types.AugmentedCDP{}, err
}
// total debt is the sum of all oustanding principal and fees
var totalDebt int64
for _, principalCoin := range cdp.Principal {
totalDebt += principalCoin.Amount.Int64()
}
for _, feeCoin := range cdp.AccumulatedFees.Add(fees...) {
totalDebt += feeCoin.Amount.Int64()
}
totalDebt += cdp.Principal.Amount.Int64()
totalDebt += cdp.AccumulatedFees.Amount.Int64()
// convert collateral value to debt coin
debtBaseAdjusted := sdk.NewDec(totalDebt).QuoInt64(BaseDigitFactor)
collateralValueInDebtDenom := collateralizationRatio.Mul(debtBaseAdjusted)
collateralValueInDebt := sdk.NewInt64Coin(cdp.Principal[0].Denom, collateralValueInDebtDenom.Int64())
collateralValueInDebt := sdk.NewInt64Coin(cdp.Principal.Denom, collateralValueInDebtDenom.Int64())
// create new augmuented cdp
augmentedCDP := types.NewAugmentedCDP(cdp, collateralValueInDebt, collateralizationRatio)
@ -460,27 +447,23 @@ func (k Keeper) LoadAugmentedCDP(ctx sdk.Context, cdp types.CDP) (types.Augmente
}
// 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, error) {
func (k Keeper) CalculateCollateralizationRatio(ctx sdk.Context, collateral sdk.Coin, principal sdk.Coin, fees sdk.Coin) (sdk.Dec, error) {
if collateral.IsZero() {
return sdk.ZeroDec(), nil
}
marketID := k.getMarketID(ctx, collateral[0].Denom)
marketID := k.getMarketID(ctx, collateral.Denom)
price, err := k.pricefeedKeeper.GetCurrentPrice(ctx, marketID)
if err != nil {
return sdk.Dec{}, err
}
collateralBaseUnits := k.convertCollateralToBaseUnits(ctx, collateral[0])
collateralBaseUnits := k.convertCollateralToBaseUnits(ctx, collateral)
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)
prinicpalBaseUnits := k.convertDebtToBaseUnits(ctx, principal)
principalTotal := prinicpalBaseUnits
feeBaseUnits := k.convertDebtToBaseUnits(ctx, fees)
principalTotal = principalTotal.Add(feeBaseUnits)
}
collateralRatio := collateralValue.Quo(principalTotal)
return collateralRatio, nil
}

View File

@ -44,27 +44,29 @@ func (suite *CdpTestSuite) TestAddCdp() {
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)))
err := suite.keeper.AddCdp(suite.ctx, addrs[0], c("xrp", 200000000), c("usdx", 26000000))
suite.Require().True(errors.Is(err, types.ErrInvalidCollateralRatio))
err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 500000000)), cs(c("usdx", 26000000)))
err = suite.keeper.AddCdp(suite.ctx, addrs[0], c("xrp", 500000000), c("usdx", 26000000))
suite.Error(err) // insufficient balance
err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 200000000)), cs(c("xusd", 10000000)))
err = suite.keeper.AddCdp(suite.ctx, addrs[0], c("xrp", 200000000), c("xusd", 10000000))
suite.Require().True(errors.Is(err, types.ErrDebtNotSupported))
acc2 := ak.NewAccountWithAddress(suite.ctx, addrs[1])
acc2.SetCoins(cs(c("btc", 500000000000)))
ak.SetAccount(suite.ctx, acc2)
err = suite.keeper.AddCdp(suite.ctx, addrs[1], cs(c("btc", 500000000000)), cs(c("usdx", 500000000001)))
err = suite.keeper.AddCdp(suite.ctx, addrs[1], c("btc", 500000000000), c("usdx", 500000000001))
suite.Require().True(errors.Is(err, types.ErrExceedsDebtLimit))
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)))
err = pk.SetCurrentPrices(ctx, "xrp:usd")
suite.Error(err)
err = suite.keeper.AddCdp(ctx, addrs[0], c("xrp", 100000000), c("usdx", 10000000))
suite.Error(err) // no prices in pricefeed
_ = pk.SetCurrentPrices(suite.ctx, "xrp:usd")
err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 100000000)), cs(c("usdx", 10000000)))
err = pk.SetCurrentPrices(suite.ctx, "xrp:usd")
suite.NoError(err)
err = suite.keeper.AddCdp(suite.ctx, addrs[0], c("xrp", 100000000), c("usdx", 10000000))
suite.NoError(err)
id := suite.keeper.GetNextCdpID(suite.ctx)
suite.Equal(uint64(2), id)
@ -76,10 +78,10 @@ func (suite *CdpTestSuite) TestAddCdp() {
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)))
err = suite.keeper.AddCdp(suite.ctx, addrs[0], c("btc", 500000000), c("usdx", 26667000000))
suite.Require().True(errors.Is(err, types.ErrInvalidCollateralRatio))
err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("btc", 500000000)), cs(c("usdx", 100000000)))
err = suite.keeper.AddCdp(suite.ctx, addrs[0], c("btc", 500000000), c("usdx", 100000000))
suite.NoError(err)
id = suite.keeper.GetNextCdpID(suite.ctx)
suite.Equal(uint64(3), id)
@ -90,9 +92,9 @@ func (suite *CdpTestSuite) TestAddCdp() {
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)))
err = suite.keeper.AddCdp(suite.ctx, addrs[0], c("lol", 100), c("usdx", 10))
suite.Require().True(errors.Is(err, types.ErrCollateralNotSupported))
err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 100)), cs(c("usdx", 10)))
err = suite.keeper.AddCdp(suite.ctx, addrs[0], c("xrp", 100), c("usdx", 10))
suite.Require().True(errors.Is(err, types.ErrCdpAlreadyExists))
}
@ -120,8 +122,9 @@ func (suite *CdpTestSuite) TestGetNextCdpID() {
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)
cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], c("xrp", 1), c("usdx", 1), tmtime.Canonical(time.Now()))
err := suite.keeper.SetCDP(suite.ctx, cdp)
suite.NoError(err)
t, found := suite.keeper.GetCDP(suite.ctx, "xrp", types.DefaultCdpStartingID)
suite.True(found)
@ -135,8 +138,9 @@ func (suite *CdpTestSuite) TestGetSetCdp() {
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)
cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], c("xrp", 1), c("usdx", 1), tmtime.Canonical(time.Now()))
err := suite.keeper.SetCDP(suite.ctx, cdp)
suite.NoError(err)
suite.keeper.IndexCdpByOwner(suite.ctx, cdp)
id, found := suite.keeper.GetCdpID(suite.ctx, addrs[0], "xrp")
suite.True(found)
@ -149,8 +153,9 @@ func (suite *CdpTestSuite) TestGetSetCdpId() {
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)
cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], c("xrp", 1), c("usdx", 1), tmtime.Canonical(time.Now()))
err := suite.keeper.SetCDP(suite.ctx, cdp)
suite.NoError(err)
suite.keeper.IndexCdpByOwner(suite.ctx, cdp)
t, found := suite.keeper.GetCdpByOwnerAndDenom(suite.ctx, addrs[0], "xrp")
suite.True(found)
@ -164,31 +169,29 @@ func (suite *CdpTestSuite) TestGetSetCdpByOwnerAndDenom() {
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()))
cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], c("xrp", 3), 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()))
cdp = types.NewCDP(types.DefaultCdpStartingID, addrs[0], c("xrp", 1), 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()))
cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], c("xrp", 3), 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) })
suite.NotPanics(func() { suite.keeper.IndexCdpByCollateralRatio(suite.ctx, cdp.Collateral.Denom, cdp.ID, cr) })
}
func (suite *CdpTestSuite) TestIterateCdps() {
cdps := cdps()
for _, c := range cdps {
suite.keeper.SetCDP(suite.ctx, c)
err := suite.keeper.SetCDP(suite.ctx, c)
suite.NoError(err)
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)
suite.keeper.IndexCdpByCollateralRatio(suite.ctx, c.Collateral.Denom, c.ID, cr)
}
t := suite.keeper.GetAllCdps(suite.ctx)
suite.Equal(4, len(t))
@ -197,10 +200,11 @@ func (suite *CdpTestSuite) TestIterateCdps() {
func (suite *CdpTestSuite) TestIterateCdpsByDenom() {
cdps := cdps()
for _, c := range cdps {
suite.keeper.SetCDP(suite.ctx, c)
err := suite.keeper.SetCDP(suite.ctx, c)
suite.NoError(err)
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)
suite.keeper.IndexCdpByCollateralRatio(suite.ctx, c.Collateral.Denom, c.ID, cr)
}
xrpCdps := suite.keeper.GetAllCdpsByDenom(suite.ctx, "xrp")
suite.Equal(3, len(xrpCdps))
@ -221,10 +225,11 @@ func (suite *CdpTestSuite) TestIterateCdpsByDenom() {
func (suite *CdpTestSuite) TestIterateCdpsByCollateralRatio() {
cdps := cdps()
for _, c := range cdps {
suite.keeper.SetCDP(suite.ctx, c)
err := suite.keeper.SetCDP(suite.ctx, c)
suite.NoError(err)
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)
suite.keeper.IndexCdpByCollateralRatio(suite.ctx, c.Collateral.Denom, c.ID, cr)
}
xrpCdps := suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("1.25"))
suite.Equal(0, len(xrpCdps))
@ -237,51 +242,46 @@ func (suite *CdpTestSuite) TestIterateCdpsByCollateralRatio() {
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)
suite.keeper.RemoveCdpCollateralRatioIndex(suite.ctx, cdps[0].Collateral.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)))
c := 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)))
c = sdk.NewCoin("lol", sdk.NewInt(1))
err = suite.keeper.ValidateCollateral(suite.ctx, c)
suite.Require().True(errors.Is(err, types.ErrCollateralNotSupported))
c = sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(1)), sdk.NewCoin("xrp", sdk.NewInt(1)))
err = suite.keeper.ValidateCollateral(suite.ctx, c)
suite.Require().True(errors.Is(err, types.ErrInvalidCollateralLength))
}
func (suite *CdpTestSuite) TestValidatePrincipal() {
d := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(10000000)))
d := 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)))
d = sdk.NewCoin("xusd", sdk.NewInt(1))
err = suite.keeper.ValidatePrincipalAdd(suite.ctx, d)
suite.Require().True(errors.Is(err, types.ErrDebtNotSupported))
d = sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(1000000000001)))
d = sdk.NewCoin("usdx", sdk.NewInt(1000000000001))
err = suite.keeper.ValidateDebtLimit(suite.ctx, "xrp", d)
suite.Require().True(errors.Is(err, types.ErrExceedsDebtLimit))
d = sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(100000000)))
d = sdk.NewCoin("usdx", sdk.NewInt(100000000))
err = suite.keeper.ValidateDebtLimit(suite.ctx, "xrp", d)
suite.NoError(err)
}
func (suite *CdpTestSuite) TestCalculateCollateralizationRatio() {
c := cdps()[1]
suite.keeper.SetCDP(suite.ctx, c)
err := suite.keeper.SetCDP(suite.ctx, c)
suite.NoError(err)
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.keeper.IndexCdpByCollateralRatio(suite.ctx, c.Collateral.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)))
c.AccumulatedFees = 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)

View File

@ -10,23 +10,23 @@ import (
)
// DepositCollateral adds collateral to a cdp
func (k Keeper) DepositCollateral(ctx sdk.Context, owner sdk.AccAddress, depositor sdk.AccAddress, collateral sdk.Coins) error {
func (k Keeper) DepositCollateral(ctx sdk.Context, owner sdk.AccAddress, depositor sdk.AccAddress, collateral sdk.Coin) error {
err := k.ValidateCollateral(ctx, collateral)
if err != nil {
return err
}
cdp, found := k.GetCdpByOwnerAndDenom(ctx, owner, collateral[0].Denom)
cdp, found := k.GetCdpByOwnerAndDenom(ctx, owner, collateral.Denom)
if !found {
return sdkerrors.Wrapf(types.ErrCdpNotFound, "owner %s, collateral %s", owner, collateral[0].Denom)
return sdkerrors.Wrapf(types.ErrCdpNotFound, "owner %s, collateral %s", owner, collateral.Denom)
}
deposit, found := k.GetDeposit(ctx, cdp.ID, depositor)
if found {
deposit.Amount = deposit.Amount.Add(collateral...)
deposit.Amount = deposit.Amount.Add(collateral)
} else {
deposit = types.NewDeposit(cdp.ID, depositor, collateral)
}
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, depositor, types.ModuleName, collateral)
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, depositor, types.ModuleName, sdk.NewCoins(collateral))
if err != nil {
return err
}
@ -40,31 +40,33 @@ func (k Keeper) DepositCollateral(ctx sdk.Context, owner sdk.AccAddress, deposit
k.SetDeposit(ctx, deposit)
oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees...))
k.RemoveCdpCollateralRatioIndex(ctx, cdp.Collateral[0].Denom, cdp.ID, oldCollateralToDebtRatio)
oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
k.RemoveCdpCollateralRatioIndex(ctx, cdp.Collateral.Denom, cdp.ID, oldCollateralToDebtRatio)
cdp.FeesUpdated = ctx.BlockTime()
cdp.Collateral = cdp.Collateral.Add(collateral...)
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees...))
k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
cdp.Collateral = cdp.Collateral.Add(collateral)
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
err = k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
if err != nil {
return err
}
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) error {
func (k Keeper) WithdrawCollateral(ctx sdk.Context, owner sdk.AccAddress, depositor sdk.AccAddress, collateral sdk.Coin) error {
err := k.ValidateCollateral(ctx, collateral)
if err != nil {
return err
}
cdp, found := k.GetCdpByOwnerAndDenom(ctx, owner, collateral[0].Denom)
cdp, found := k.GetCdpByOwnerAndDenom(ctx, owner, collateral.Denom)
if !found {
return sdkerrors.Wrapf(types.ErrCdpNotFound, "owner %s, collateral %s", owner, collateral[0].Denom)
return sdkerrors.Wrapf(types.ErrCdpNotFound, "owner %s, collateral %s", owner, collateral.Denom)
}
deposit, found := k.GetDeposit(ctx, cdp.ID, depositor)
if !found {
return sdkerrors.Wrapf(types.ErrDepositNotFound, "depositor %s, collateral %s", depositor, collateral[0].Denom)
return sdkerrors.Wrapf(types.ErrDepositNotFound, "depositor %s, collateral %s", depositor, collateral.Denom)
}
if collateral.IsAnyGT(deposit.Amount) {
if collateral.Amount.GT(deposit.Amount.Amount) {
return sdkerrors.Wrapf(types.ErrInvalidWithdrawAmount, "collateral %s, deposit %s", collateral, deposit.Amount)
}
@ -72,9 +74,9 @@ func (k Keeper) WithdrawCollateral(ctx sdk.Context, owner sdk.AccAddress, deposi
if err != nil {
return err
}
liquidationRatio := k.getLiquidationRatio(ctx, collateral[0].Denom)
liquidationRatio := k.getLiquidationRatio(ctx, collateral.Denom)
if collateralizationRatio.LT(liquidationRatio) {
return sdkerrors.Wrapf(types.ErrInvalidCollateralRatio, "collateral %s, collateral ratio %s, liquidation ration %s", collateral[0].Denom, collateralizationRatio, liquidationRatio)
return sdkerrors.Wrapf(types.ErrInvalidCollateralRatio, "collateral %s, collateral ratio %s, liquidation ration %s", collateral.Denom, collateralizationRatio, liquidationRatio)
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
@ -84,17 +86,19 @@ func (k Keeper) WithdrawCollateral(ctx sdk.Context, owner sdk.AccAddress, deposi
),
)
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, depositor, collateral)
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, depositor, sdk.NewCoins(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)
oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
k.RemoveCdpCollateralRatioIndex(ctx, cdp.Collateral.Denom, cdp.ID, oldCollateralToDebtRatio)
cdp.FeesUpdated = ctx.BlockTime()
cdp.Collateral = cdp.Collateral.Sub(collateral)
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees...))
k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
err = k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
if err != nil {
return err
}
deposit.Amount = deposit.Amount.Sub(collateral)
if deposit.Amount.IsZero() {

View File

@ -41,14 +41,14 @@ func (suite *DepositTestSuite) SetupTest() {
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)))
err := suite.keeper.AddCdp(suite.ctx, addrs[0], c("xrp", 400000000), c("usdx", 10000000))
suite.NoError(err)
}
func (suite *DepositTestSuite) TestGetSetDeposit() {
d, found := suite.keeper.GetDeposit(suite.ctx, uint64(1), suite.addrs[0])
suite.True(found)
td := types.NewDeposit(uint64(1), suite.addrs[0], cs(c("xrp", 400000000)))
td := types.NewDeposit(uint64(1), suite.addrs[0], c("xrp", 400000000))
suite.True(d.Equals(td))
ds := suite.keeper.GetDeposits(suite.ctx, uint64(1))
suite.Equal(1, len(ds))
@ -61,32 +61,32 @@ func (suite *DepositTestSuite) TestGetSetDeposit() {
}
func (suite *DepositTestSuite) TestDepositCollateral() {
err := suite.keeper.DepositCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 10000000)))
err := suite.keeper.DepositCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], c("xrp", 10000000))
suite.NoError(err)
d, found := suite.keeper.GetDeposit(suite.ctx, uint64(1), suite.addrs[0])
suite.True(found)
td := types.NewDeposit(uint64(1), suite.addrs[0], cs(c("xrp", 410000000)))
td := types.NewDeposit(uint64(1), suite.addrs[0], 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)
suite.Equal(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)))
err = suite.keeper.DepositCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], c("btc", 1))
suite.Require().True(errors.Is(err, types.ErrCdpNotFound))
err = suite.keeper.DepositCollateral(suite.ctx, suite.addrs[1], suite.addrs[0], cs(c("xrp", 1)))
err = suite.keeper.DepositCollateral(suite.ctx, suite.addrs[1], suite.addrs[0], c("xrp", 1))
suite.Require().True(errors.Is(err, types.ErrCdpNotFound))
err = suite.keeper.DepositCollateral(suite.ctx, suite.addrs[0], suite.addrs[1], cs(c("xrp", 10000000)))
err = suite.keeper.DepositCollateral(suite.ctx, suite.addrs[0], suite.addrs[1], c("xrp", 10000000))
suite.NoError(err)
d, found = suite.keeper.GetDeposit(suite.ctx, uint64(1), suite.addrs[1])
suite.True(found)
td = types.NewDeposit(uint64(1), suite.addrs[1], cs(c("xrp", 10000000)))
td = types.NewDeposit(uint64(1), suite.addrs[1], c("xrp", 10000000))
suite.True(d.Equals(td))
ds = suite.keeper.GetDeposits(suite.ctx, uint64(1))
suite.Equal(2, len(ds))
@ -94,29 +94,30 @@ func (suite *DepositTestSuite) TestDepositCollateral() {
}
func (suite *DepositTestSuite) TestWithdrawCollateral() {
err := suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 400000000)))
err := suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], c("xrp", 400000000))
suite.Require().True(errors.Is(err, types.ErrInvalidCollateralRatio))
err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 321000000)))
err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], c("xrp", 321000000))
suite.Require().True(errors.Is(err, types.ErrInvalidCollateralRatio))
err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[1], suite.addrs[0], cs(c("xrp", 10000000)))
err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[1], suite.addrs[0], c("xrp", 10000000))
suite.Require().True(errors.Is(err, types.ErrCdpNotFound))
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)))
cd.AccumulatedFees = c("usdx", 1)
err = suite.keeper.SetCDP(suite.ctx, cd)
suite.NoError(err)
err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], c("xrp", 320000000))
suite.Require().True(errors.Is(err, types.ErrInvalidCollateralRatio))
err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 10000000)))
err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], c("xrp", 10000000))
suite.NoError(err)
dep, _ := suite.keeper.GetDeposit(suite.ctx, uint64(1), suite.addrs[0])
td := types.NewDeposit(uint64(1), suite.addrs[0], cs(c("xrp", 390000000)))
td := types.NewDeposit(uint64(1), suite.addrs[0], c("xrp", 390000000))
suite.True(dep.Equals(td))
ak := suite.app.GetAccountKeeper()
acc := ak.GetAccount(suite.ctx, suite.addrs[0])
suite.Equal(i(110000000), acc.GetCoins().AmountOf("xrp"))
err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[1], cs(c("xrp", 10000000)))
err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[1], c("xrp", 10000000))
suite.Require().True(errors.Is(err, types.ErrDepositNotFound))
}

View File

@ -9,33 +9,33 @@ import (
)
// 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) error {
func (k Keeper) AddPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom string, principal sdk.Coin) error {
// validation
cdp, found := k.GetCdpByOwnerAndDenom(ctx, owner, denom)
if !found {
return sdkerrors.Wrapf(types.ErrCdpNotFound, "owner %s, denom %s", owner, denom)
}
err := k.ValidatePrincipalDraw(ctx, principal)
err := k.ValidatePrincipalDraw(ctx, principal, cdp.Principal.Denom)
if err != nil {
return err
}
err = k.ValidateDebtLimit(ctx, cdp.Collateral[0].Denom, principal)
err = k.ValidateDebtLimit(ctx, cdp.Collateral.Denom, principal)
if err != nil {
return err
}
err = k.ValidateCollateralizationRatio(ctx, cdp.Collateral, cdp.Principal.Add(principal...), cdp.AccumulatedFees)
err = k.ValidateCollateralizationRatio(ctx, cdp.Collateral, cdp.Principal.Add(principal), cdp.AccumulatedFees)
if err != nil {
return err
}
// mint the principal and send it to the cdp owner
err = k.supplyKeeper.MintCoins(ctx, types.ModuleName, principal)
err = k.supplyKeeper.MintCoins(ctx, types.ModuleName, sdk.NewCoins(principal))
if err != nil {
panic(err)
}
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, owner, principal)
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, owner, sdk.NewCoins(principal))
if err != nil {
panic(err)
}
@ -56,61 +56,59 @@ func (k Keeper) AddPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom string
)
// remove old collateral:debt index
oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees...))
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.FeesUpdated = ctx.BlockTime()
cdp.Principal = cdp.Principal.Add(principal)
// increment total principal for the input collateral type
k.IncrementTotalPrincipal(ctx, cdp.Collateral[0].Denom, principal)
k.IncrementTotalPrincipal(ctx, cdp.Collateral.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)
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
err = k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
if err != nil {
return err
}
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) error {
func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom string, payment sdk.Coin) error {
// validation
cdp, found := k.GetCdpByOwnerAndDenom(ctx, owner, denom)
if !found {
return sdkerrors.Wrapf(types.ErrCdpNotFound, "owner %s, denom %s", owner, denom)
}
err := k.ValidatePaymentCoins(ctx, cdp, payment, cdp.Principal.Add(cdp.AccumulatedFees...))
err := k.ValidatePaymentCoins(ctx, cdp, payment, cdp.Principal.Add(cdp.AccumulatedFees))
if err != nil {
return err
}
// calculate fee and principal payment
feePayment, principalPayment := k.calculatePayment(ctx, cdp.Principal.Add(cdp.AccumulatedFees...), cdp.AccumulatedFees, payment)
feePayment, principalPayment := k.calculatePayment(ctx, cdp.Principal.Add(cdp.AccumulatedFees), cdp.AccumulatedFees, payment)
// send the payment from the sender to the cpd module
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, feePayment.Add(principalPayment...))
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, sdk.NewCoins(feePayment.Add(principalPayment)))
if err != nil {
return err
}
// burn the payment coins
err = k.supplyKeeper.BurnCoins(ctx, types.ModuleName, feePayment.Add(principalPayment...))
err = k.supplyKeeper.BurnCoins(ctx, types.ModuleName, sdk.NewCoins(feePayment.Add(principalPayment)))
if err != nil {
panic(err)
}
// burn the corresponding amount of debt coins
cdpDebt := k.getModAccountDebt(ctx, types.ModuleName)
paymentAmount := sdk.ZeroInt()
for _, c := range feePayment.Add(principalPayment...) {
paymentAmount = paymentAmount.Add(c.Amount)
}
coinsToBurn := sdk.NewCoins(sdk.NewCoin(k.GetDebtDenom(ctx), paymentAmount))
paymentAmount := feePayment.Amount.Add(principalPayment.Amount)
coinsToBurn := sdk.NewCoin(k.GetDebtDenom(ctx), paymentAmount)
if paymentAmount.GT(cdpDebt) {
coinsToBurn = sdk.NewCoins(sdk.NewCoin(k.GetDebtDenom(ctx), cdpDebt))
coinsToBurn = sdk.NewCoin(k.GetDebtDenom(ctx), cdpDebt)
}
err = k.BurnDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), coinsToBurn)
if err != nil {
@ -121,13 +119,13 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom stri
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeCdpRepay,
sdk.NewAttribute(sdk.AttributeKeyAmount, feePayment.Add(principalPayment...).String()),
sdk.NewAttribute(sdk.AttributeKeyAmount, feePayment.Add(principalPayment).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...))
oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
k.RemoveCdpCollateralRatioIndex(ctx, denom, cdp.ID, oldCollateralToDebtRatio)
// update cdp state
@ -135,10 +133,9 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom stri
cdp.Principal = cdp.Principal.Sub(principalPayment)
}
cdp.AccumulatedFees = cdp.AccumulatedFees.Sub(feePayment)
cdp.FeesUpdated = ctx.BlockTime()
// decrement the total principal for the input collateral type
k.DecrementTotalPrincipal(ctx, denom, feePayment.Add(principalPayment...))
k.DecrementTotalPrincipal(ctx, denom, feePayment.Add(principalPayment))
// if the debt is fully paid, return collateral to depositors,
// and remove the cdp and indexes from the store
@ -158,31 +155,26 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom stri
}
// set cdp state and update indexes
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees...))
k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
err = k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
if err != nil {
return err
}
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, debt sdk.Coins) error {
subset := payment.DenomsSubsetOf(cdp.Principal)
if !subset {
var paymentDenoms []string
var principalDenoms []string
for _, pc := range cdp.Principal {
principalDenoms = append(principalDenoms, pc.Denom)
func (k Keeper) ValidatePaymentCoins(ctx sdk.Context, cdp types.CDP, payment sdk.Coin, debt sdk.Coin) error {
if payment.Denom != debt.Denom {
return sdkerrors.Wrapf(types.ErrInvalidPayment, "cdp %d: expected %s, got %s", cdp.ID, debt.Denom, payment.Denom)
}
for _, pc := range payment {
paymentDenoms = append(paymentDenoms, pc.Denom)
dp, found := k.GetDebtParam(ctx, payment.Denom)
if !found {
return sdkerrors.Wrapf(types.ErrInvalidPayment, "payment denom %s not found", payment.Denom)
}
return sdkerrors.Wrapf(types.ErrInvalidPayment, "cdp %d: expected %s, got %s", cdp.ID, principalDenoms, paymentDenoms)
}
for _, dc := range payment {
dp, _ := k.GetDebtParam(ctx, dc.Denom)
proposedBalance := cdp.Principal.AmountOf(dc.Denom).Sub(dc.Amount)
proposedBalance := cdp.Principal.Amount.Sub(payment.Amount)
if proposedBalance.GT(sdk.ZeroInt()) && proposedBalance.LT(dp.DebtFloor) {
return sdkerrors.Wrapf(types.ErrBelowDebtFloor, "proposed %s < minimum %s", sdk.NewCoins(sdk.NewCoin(dc.Denom, proposedBalance)), dp.DebtFloor)
}
return sdkerrors.Wrapf(types.ErrBelowDebtFloor, "proposed %s < minimum %s", sdk.NewCoin(payment.Denom, proposedBalance), dp.DebtFloor)
}
return nil
}
@ -191,7 +183,7 @@ func (k Keeper) ValidatePaymentCoins(ctx sdk.Context, cdp types.CDP, payment sdk
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)
err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, deposit.Depositor, sdk.NewCoins(deposit.Amount))
if err != nil {
panic(err)
}
@ -199,31 +191,30 @@ func (k Keeper) ReturnCollateral(ctx sdk.Context, cdp types.CDP) {
}
}
func (k Keeper) calculatePayment(ctx sdk.Context, owed sdk.Coins, fees sdk.Coins, payment sdk.Coins) (sdk.Coins, sdk.Coins) {
func (k Keeper) calculatePayment(ctx sdk.Context, owed sdk.Coin, fees sdk.Coin, payment sdk.Coin) (sdk.Coin, sdk.Coin) {
// divides repayment into principal and fee components, with fee payment applied first.
feePayment := sdk.NewCoins()
principalPayment := sdk.NewCoins()
overpayment := sdk.NewCoins()
// TODO must compare denoms directly if there are multiple principal denoms
if payment.IsAllGT(owed) {
feePayment := sdk.NewCoin(payment.Denom, sdk.ZeroInt())
principalPayment := sdk.NewCoin(payment.Denom, sdk.ZeroInt())
overpayment := sdk.NewCoin(payment.Denom, sdk.ZeroInt())
if !payment.Amount.IsPositive() {
return feePayment, principalPayment
}
// check for over payment
if payment.Amount.GT(owed.Amount) {
overpayment = payment.Sub(owed)
payment = payment.Sub(overpayment)
}
// if no fees, 100% of payment is principal payment
if fees.IsZero() {
return sdk.NewCoins(), payment
return feePayment, payment
}
for _, fc := range fees {
if payment.AmountOf(fc.Denom).IsPositive() {
if payment.AmountOf(fc.Denom).GT(fc.Amount) {
feePayment = feePayment.Add(fc)
pc := sdk.NewCoin(fc.Denom, payment.AmountOf(fc.Denom).Sub(fc.Amount))
principalPayment = principalPayment.Add(pc)
// pay fees before repaying principal
if payment.Amount.GT(fees.Amount) {
feePayment = fees
principalPayment = payment.Sub(fees)
} else {
fc := sdk.NewCoin(fc.Denom, payment.AmountOf(fc.Denom))
feePayment = feePayment.Add(fc)
}
}
feePayment = payment
}
return feePayment, principalPayment
}

View File

@ -43,18 +43,19 @@ func (suite *DrawTestSuite) SetupTest() {
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)))
err := suite.keeper.AddCdp(suite.ctx, addrs[0], c("xrp", 400000000), c("usdx", 10000000))
suite.NoError(err)
}
func (suite *DrawTestSuite) TestAddRepayPrincipal() {
err := suite.keeper.AddPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 10000000)))
err := suite.keeper.AddPrincipal(suite.ctx, suite.addrs[0], "xrp", 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...))
t, found := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1))
suite.True(found)
suite.Equal(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))
@ -66,74 +67,44 @@ func (suite *DrawTestSuite) TestAddRepayPrincipal() {
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[0], "xrp", c("susd", 10000000))
suite.Require().True(errors.Is(err, types.ErrInvalidDebtRequest))
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[1], "xrp", cs(c("usdx", 10000000)))
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[1], "xrp", c("usdx", 10000000))
suite.Require().True(errors.Is(err, types.ErrCdpNotFound))
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("xusd", 10000000)))
suite.Require().True(errors.Is(err, types.ErrDebtNotSupported))
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 311000000)))
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[0], "xrp", c("xusd", 10000000))
suite.Require().True(errors.Is(err, types.ErrInvalidDebtRequest))
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[0], "xrp", c("usdx", 311000000))
suite.Require().True(errors.Is(err, types.ErrInvalidCollateralRatio))
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 10000000)))
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", 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())
t, found = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1))
suite.True(found)
suite.Equal(c("usdx", 10000000), t.Principal)
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...))
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)))
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", c("xusd", 10000000))
suite.Require().True(errors.Is(err, types.ErrInvalidPayment))
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[1], "xrp", cs(c("xusd", 10000000)))
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[1], "xrp", c("xusd", 10000000))
suite.Require().True(errors.Is(err, types.ErrCdpNotFound))
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 9000000)))
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", c("usdx", 9000000))
suite.Require().True(errors.Is(err, types.ErrBelowDebtFloor))
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 10000000)))
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", c("usdx", 10000000))
suite.NoError(err)
_, found := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1))
_, 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))
@ -146,7 +117,7 @@ func (suite *DrawTestSuite) TestAddRepayPrincipal() {
}
func (suite *DrawTestSuite) TestRepayPrincipalOverpay() {
err := suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 20000000)))
err := suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", c("usdx", 20000000))
suite.NoError(err)
ak := suite.app.GetAccountKeeper()
acc := ak.GetAccount(suite.ctx, suite.addrs[0])
@ -156,43 +127,43 @@ func (suite *DrawTestSuite) TestRepayPrincipalOverpay() {
}
func (suite *DrawTestSuite) TestAddRepayPrincipalFees() {
err := suite.keeper.AddCdp(suite.ctx, suite.addrs[2], cs(c("xrp", 1000000000000)), cs(c("usdx", 100000000000)))
err := suite.keeper.AddCdp(suite.ctx, suite.addrs[2], c("xrp", 1000000000000), c("usdx", 100000000000))
suite.NoError(err)
suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Minute * 10))
err = suite.keeper.UpdateFeesForAllCdps(suite.ctx, "xrp")
suite.NoError(err)
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[2], "xrp", cs(c("usdx", 10000000)))
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[2], "xrp", c("usdx", 10000000))
suite.NoError(err)
t, _ := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2))
suite.Equal(cs(c("usdx", 92827)), t.AccumulatedFees)
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[2], "xrp", cs(c("usdx", 100)))
suite.Equal(c("usdx", 92827), t.AccumulatedFees)
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[2], "xrp", 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.Equal(c("usdx", 92727), t.AccumulatedFees)
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[2], "xrp", 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)))
err = suite.keeper.AddCdp(suite.ctx, suite.addrs[2], c("xrp", 1000000000000), c("usdx", 100000000))
suite.NoError(err)
suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Second * 31536000)) // move forward one year in time
err = suite.keeper.UpdateFeesForAllCdps(suite.ctx, "xrp")
suite.NoError(err)
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[2], "xrp", cs(c("usdx", 100000000)))
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[2], "xrp", c("usdx", 100000000))
suite.NoError(err)
t, _ = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(3))
suite.Equal(cs(c("usdx", 5000000)), t.AccumulatedFees)
suite.Equal(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", 10000000)))
err := suite.keeper.AddPrincipal(ctx, suite.addrs[0], "xrp", c("usdx", 10000000))
suite.Error(err)
err = suite.keeper.RepayPrincipal(ctx, suite.addrs[0], "xrp", cs(c("usdx", 10000000)))
err = suite.keeper.RepayPrincipal(ctx, suite.addrs[0], "xrp", c("usdx", 10000000))
suite.NoError(err)
}
@ -203,7 +174,7 @@ func (suite *DrawTestSuite) TestModuleAccountFailure() {
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", 10000000)))
suite.keeper.RepayPrincipal(ctx, suite.addrs[0], "xrp", c("usdx", 10000000))
})
}

View File

@ -8,9 +8,7 @@ import (
// 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 {
func (k Keeper) CalculateFees(ctx sdk.Context, principal sdk.Coin, periods sdk.Int, denom string) sdk.Coin {
// 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
@ -18,19 +16,17 @@ func (k Keeper) CalculateFees(ctx sdk.Context, principal sdk.Coins, periods sdk.
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.NewCoin(pc.Denom, feesAccumulated.TruncateInt()))
}
feesAccumulated := (sdk.NewDecFromInt(principal.Amount).Mul(accumulator)).Sub(sdk.NewDecFromInt(principal.Amount))
newFees := sdk.NewCoin(principal.Denom, feesAccumulated.TruncateInt())
return newFees
}
// UpdateFeesForAllCdps updates the fees for each of the CDPs
func (k Keeper) UpdateFeesForAllCdps(ctx sdk.Context, collateralDenom string) error {
var iterationErr error
k.IterateCdpsByDenom(ctx, collateralDenom, func(cdp types.CDP) bool {
oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees...))
oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix()))
newFees := k.CalculateFees(ctx, cdp.Principal, periods, collateralDenom)
@ -42,15 +38,14 @@ func (k Keeper) UpdateFeesForAllCdps(ctx sdk.Context, collateralDenom string) er
}
// note - only works if principal length is one
for _, dc := range cdp.Principal {
dp, found := k.GetDebtParam(ctx, dc.Denom)
dp, found := k.GetDebtParam(ctx, cdp.Principal.Denom)
if !found {
return false
}
savingsRate := dp.SavingsRate
newFeesSavings := sdk.NewDecFromInt(newFees.AmountOf(dp.Denom)).Mul(savingsRate).RoundInt()
newFeesSurplus := newFees.AmountOf(dp.Denom).Sub(newFeesSavings)
newFeesSavings := sdk.NewDecFromInt(newFees.Amount).Mul(savingsRate).RoundInt()
newFeesSurplus := newFees.Amount.Sub(newFeesSavings)
// similar to checking for rounding to zero of all fees, but in this case we
// need to handle cases where we expect surplus or savings fees to be zero, namely
@ -62,47 +57,50 @@ func (k Keeper) UpdateFeesForAllCdps(ctx sdk.Context, collateralDenom string) er
// mint debt coins to the cdp account
k.MintDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), newFees)
previousDebt := k.GetTotalPrincipal(ctx, collateralDenom, dp.Denom)
feeCoins := sdk.NewCoins(sdk.NewCoin(dp.Denom, previousDebt))
k.SetTotalPrincipal(ctx, collateralDenom, dp.Denom, feeCoins.Add(newFees...).AmountOf(dp.Denom))
newDebt := previousDebt.Add(newFees.Amount)
k.SetTotalPrincipal(ctx, collateralDenom, dp.Denom, newDebt)
// mint surplus coins divided between the liquidator and savings module accounts.
k.supplyKeeper.MintCoins(ctx, types.LiquidatorMacc, sdk.NewCoins(sdk.NewCoin(dp.Denom, newFeesSurplus)))
k.supplyKeeper.MintCoins(ctx, types.SavingsRateMacc, sdk.NewCoins(sdk.NewCoin(dp.Denom, newFeesSavings)))
}
// now add the new fees fees to the accumulated fees for the cdp
cdp.AccumulatedFees = cdp.AccumulatedFees.Add(newFees...)
cdp.AccumulatedFees = cdp.AccumulatedFees.Add(newFees)
// and set the fees updated time to the current block time since we just updated it
cdp.FeesUpdated = ctx.BlockTime()
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees...))
k.RemoveCdpCollateralRatioIndex(ctx, cdp.Collateral[0].Denom, cdp.ID, oldCollateralToDebtRatio)
k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
k.RemoveCdpCollateralRatioIndex(ctx, cdp.Collateral.Denom, cdp.ID, oldCollateralToDebtRatio)
err := k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
if err != nil {
iterationErr = err
return true
}
return false // this returns true when you want to stop iterating. Since we want to iterate through all we return false
})
if iterationErr != nil {
return iterationErr
}
return nil
}
// 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)
}
func (k Keeper) IncrementTotalPrincipal(ctx sdk.Context, collateralDenom string, principal sdk.Coin) {
total := k.GetTotalPrincipal(ctx, collateralDenom, principal.Denom)
total = total.Add(principal.Amount)
k.SetTotalPrincipal(ctx, collateralDenom, principal.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)
func (k Keeper) DecrementTotalPrincipal(ctx sdk.Context, collateralDenom string, principal sdk.Coin) {
total := k.GetTotalPrincipal(ctx, collateralDenom, principal.Denom)
total = total.Sub(principal.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)
}
k.SetTotalPrincipal(ctx, collateralDenom, principal.Denom, total)
}
// GetTotalPrincipal returns the total amount of principal that has been drawn for a particular collateral

View File

@ -1,12 +1,10 @@
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"
@ -36,43 +34,6 @@ func (suite *FeeTestSuite) SetupTest() {
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(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.True(d("0.00001").GTE(absError))
}
}
// createCdps is a helper function to create two CDPs each with zero fees
func (suite *FeeTestSuite) createCdps() {
// create 2 accounts in the state and give them some coins
@ -91,11 +52,14 @@ func (suite *FeeTestSuite) createCdps() {
// now create two cdps with the addresses we just created
// use the created account to create a cdp that SHOULD have fees updated
err := suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 200000000)), cs(c("usdx", 24000000)))
// to get a ratio between 100 - 110% of liquidation ratio we can use 200xrp ($50) and 24 usdx (208% collateralization with liquidation ratio of 200%)
// create CDP for the first address
err := suite.keeper.AddCdp(suite.ctx, addrs[0], c("xrp", 200000000), c("usdx", 24000000))
suite.NoError(err) // check that no error was thrown
// use the other account to create a cdp that SHOULD NOT have fees updated
err = suite.keeper.AddCdp(suite.ctx, addrs[1], cs(c("xrp", 200000000)), cs(c("usdx", 10000000)))
// use the other account to create a cdp that SHOULD NOT have fees updated - 500% collateralization
// create CDP for the second address
err = suite.keeper.AddCdp(suite.ctx, addrs[1], c("xrp", 200000000), c("usdx", 10000000))
suite.NoError(err) // check that no error was thrown
}
@ -114,18 +78,19 @@ func (suite *FeeTestSuite) TestUpdateFees() {
suite.NoError(err) // check that we don't have any error
// cdp we expect fees to accumulate for
cdp1, _ := suite.keeper.GetCDP(suite.ctx, "xrp", 1)
cdp1, found := suite.keeper.GetCDP(suite.ctx, "xrp", 1)
suite.True(found)
// check fees are not zero
// check that the fees have been updated
suite.False(cdp1.AccumulatedFees.Empty())
suite.False(cdp1.AccumulatedFees.IsZero())
// now check that we have the correct amount of fees overall (22 USDX for this scenario)
suite.Equal(sdk.NewInt(22), cdp1.AccumulatedFees.AmountOf("usdx"))
suite.Equal(sdk.NewInt(22), cdp1.AccumulatedFees.Amount)
suite.Equal(suite.ctx.BlockTime(), cdp1.FeesUpdated)
// cdp we expect fees to not accumulate for because of rounding to zero
cdp2, _ := suite.keeper.GetCDP(suite.ctx, "xrp", 2)
cdp2, found := suite.keeper.GetCDP(suite.ctx, "xrp", 2)
suite.True(found)
// check fees are zero
suite.True(cdp2.AccumulatedFees.Empty())
suite.True(cdp2.AccumulatedFees.IsZero())
suite.Equal(oldtime, cdp2.FeesUpdated)
}

View File

@ -39,7 +39,7 @@ func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState {
func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
cdpGenesis := cdp.GenesisState{
Params: cdp.Params{
GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)),
GlobalDebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
DebtAuctionThreshold: cdp.DefaultDebtThreshold,
SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
@ -47,7 +47,7 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
{
Denom: asset,
LiquidationRatio: liquidationRatio,
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)),
DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr
LiquidationPenalty: d("0.05"),
AuctionSize: i(100),
@ -56,8 +56,7 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
MarketID: asset + ":usd",
},
},
DebtParams: cdp.DebtParams{
{
DebtParam: cdp.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: i(6),
@ -65,7 +64,6 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
SavingsRate: d("0.9"),
},
},
},
StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom,
GovDenom: cdp.DefaultGovDenom,
@ -103,7 +101,7 @@ func NewPricefeedGenStateMulti() app.GenesisState {
func NewCDPGenStateMulti() app.GenesisState {
cdpGenesis := cdp.GenesisState{
Params: cdp.Params{
GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)),
GlobalDebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
DebtAuctionThreshold: cdp.DefaultDebtThreshold,
SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
@ -111,7 +109,7 @@ func NewCDPGenStateMulti() app.GenesisState {
{
Denom: "xrp",
LiquidationRatio: sdk.MustNewDecFromStr("2.0"),
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)),
DebtLimit: sdk.NewInt64Coin("usdx", 500000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr
LiquidationPenalty: d("0.05"),
AuctionSize: i(7000000000),
@ -122,7 +120,7 @@ func NewCDPGenStateMulti() app.GenesisState {
{
Denom: "btc",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)),
DebtLimit: sdk.NewInt64Coin("usdx", 500000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr
LiquidationPenalty: d("0.025"),
AuctionSize: i(10000000),
@ -131,22 +129,13 @@ func NewCDPGenStateMulti() app.GenesisState {
ConversionFactor: i(8),
},
},
DebtParams: cdp.DebtParams{
{
DebtParam: cdp.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
SavingsRate: d("0.95"),
},
{
Denom: "susd",
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
SavingsRate: d("0.95"),
},
},
},
StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom,
@ -160,7 +149,7 @@ func NewCDPGenStateMulti() app.GenesisState {
func NewCDPGenStateHighDebtLimit() app.GenesisState {
cdpGenesis := cdp.GenesisState{
Params: cdp.Params{
GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 100000000000000), sdk.NewInt64Coin("susd", 100000000000000)),
GlobalDebtLimit: sdk.NewInt64Coin("usdx", 100000000000000),
SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
DebtAuctionThreshold: cdp.DefaultDebtThreshold,
SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
@ -168,7 +157,7 @@ func NewCDPGenStateHighDebtLimit() app.GenesisState {
{
Denom: "xrp",
LiquidationRatio: sdk.MustNewDecFromStr("2.0"),
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 50000000000000), sdk.NewInt64Coin("susd", 50000000000000)),
DebtLimit: sdk.NewInt64Coin("usdx", 50000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr
LiquidationPenalty: d("0.05"),
AuctionSize: i(7000000000),
@ -179,7 +168,7 @@ func NewCDPGenStateHighDebtLimit() app.GenesisState {
{
Denom: "btc",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 50000000000000), sdk.NewInt64Coin("susd", 50000000000000)),
DebtLimit: sdk.NewInt64Coin("usdx", 50000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr
LiquidationPenalty: d("0.025"),
AuctionSize: i(10000000),
@ -188,22 +177,13 @@ func NewCDPGenStateHighDebtLimit() app.GenesisState {
ConversionFactor: i(8),
},
},
DebtParams: cdp.DebtParams{
{
DebtParam: cdp.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
SavingsRate: d("0.95"),
},
{
Denom: "susd",
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
SavingsRate: d("0.95"),
},
},
},
StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom,
@ -216,10 +196,10 @@ func NewCDPGenStateHighDebtLimit() app.GenesisState {
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()))
c1 := cdp.NewCDP(uint64(1), addrs[0], sdk.NewCoin("xrp", sdk.NewInt(10000000)), sdk.NewCoin("usdx", sdk.NewInt(8000000)), tmtime.Canonical(time.Now()))
c2 := cdp.NewCDP(uint64(2), addrs[1], sdk.NewCoin("xrp", sdk.NewInt(100000000)), sdk.NewCoin("usdx", sdk.NewInt(10000000)), tmtime.Canonical(time.Now()))
c3 := cdp.NewCDP(uint64(3), addrs[1], sdk.NewCoin("btc", sdk.NewInt(1000000000)), sdk.NewCoin("usdx", sdk.NewInt(10000000)), tmtime.Canonical(time.Now()))
c4 := cdp.NewCDP(uint64(4), addrs[2], sdk.NewCoin("xrp", sdk.NewInt(1000000000)), sdk.NewCoin("usdx", sdk.NewInt(500000000)), tmtime.Canonical(time.Now()))
cdps = append(cdps, c1, c2, c3, c4)
return
}

View File

@ -14,6 +14,7 @@ import (
// saving the result to a module level variable ensures the compiler doesn't optimize the test away
var coinsResult sdk.Coins
var coinResult sdk.Coin
// Note - the iteration benchmarks take a long time to stabilize, to get stable results use:
// go test -benchmem -bench ^(BenchmarkAccountIteration)$ -benchtime 60s -timeout 2h
@ -83,7 +84,7 @@ func createCdps(n int) (app.TestApp, sdk.Context, keeper.Keeper) {
)
cdpKeeper := tApp.GetCDPKeeper()
for i := 0; i < n; i++ {
err := cdpKeeper.AddCdp(ctx, addrs[i], coins[i], cs(c("usdx", 100000000)))
err := cdpKeeper.AddCdp(ctx, addrs[i], coins[i][0], c("usdx", 100000000))
if err != nil {
panic("failed to create cdp")
}
@ -106,7 +107,7 @@ func BenchmarkCdpIteration(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
cdpKeeper.IterateAllCdps(ctx, func(c cdp.CDP) (stop bool) {
coinsResult = c.Principal
coinResult = c.Principal
return false
})
}
@ -135,7 +136,7 @@ func BenchmarkCdpCreation(b *testing.B) {
cdpKeeper := tApp.GetCDPKeeper()
b.ResetTimer()
for i := 0; i < b.N; i++ {
err := cdpKeeper.AddCdp(ctx, addrs[i], coins[i], cs(c("usdx", 100000000)))
err := cdpKeeper.AddCdp(ctx, addrs[i], coins[i][0], c("usdx", 100000000))
if err != nil {
b.Error("unexpected error")
}

View File

@ -32,12 +32,10 @@ func (k Keeper) GetCollateral(ctx sdk.Context, denom string) (types.CollateralPa
// GetDebtParam returns the debt param with matching denom
func (k Keeper) GetDebtParam(ctx sdk.Context, denom string) (types.DebtParam, bool) {
params := k.GetParams(ctx)
for _, dp := range params.DebtParams {
dp := k.GetParams(ctx).DebtParam
if dp.Denom == denom {
return dp, true
}
}
return types.DebtParam{}, false
}

View File

@ -42,7 +42,7 @@ func queryGetCdp(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte,
_, valid := keeper.GetDenomPrefix(ctx, requestParams.CollateralDenom)
if !valid {
return nil, sdkerrors.Wrap(types.ErrInvalidCollateral, requestParams.CollateralDenom)
return nil, sdkerrors.Wrap(types.ErrCollateralNotSupported, requestParams.CollateralDenom)
}
cdp, found := keeper.GetCdpByOwnerAndDenom(ctx, requestParams.Owner, requestParams.CollateralDenom)
@ -73,7 +73,7 @@ func queryGetDeposits(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]
_, valid := keeper.GetDenomPrefix(ctx, requestParams.CollateralDenom)
if !valid {
return nil, sdkerrors.Wrap(types.ErrInvalidCollateral, requestParams.CollateralDenom)
return nil, sdkerrors.Wrap(types.ErrCollateralNotSupported, requestParams.CollateralDenom)
}
cdp, found := keeper.GetCdpByOwnerAndDenom(ctx, requestParams.Owner, requestParams.CollateralDenom)
@ -100,7 +100,7 @@ func queryGetCdpsByRatio(ctx sdk.Context, req abci.RequestQuery, keeper Keeper)
}
_, valid := keeper.GetDenomPrefix(ctx, requestParams.CollateralDenom)
if !valid {
return nil, sdkerrors.Wrap(types.ErrInvalidCollateral, requestParams.CollateralDenom)
return nil, sdkerrors.Wrap(types.ErrCollateralNotSupported, requestParams.CollateralDenom)
}
ratio, err := keeper.CalculateCollateralizationRatioFromAbsoluteRatio(ctx, requestParams.CollateralDenom, requestParams.Ratio)
@ -133,7 +133,7 @@ func queryGetCdpsByDenom(ctx sdk.Context, req abci.RequestQuery, keeper Keeper)
}
_, valid := keeper.GetDenomPrefix(ctx, requestParams.CollateralDenom)
if !valid {
return nil, sdkerrors.Wrap(types.ErrInvalidCollateral, requestParams.CollateralDenom)
return nil, sdkerrors.Wrap(types.ErrCollateralNotSupported, requestParams.CollateralDenom)
}
cdps := keeper.GetAllCdpsByDenom(ctx, requestParams.CollateralDenom)

View File

@ -93,7 +93,7 @@ func (suite *QuerierTestSuite) SetupTest() {
amount = simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 500000000, 5000000000)
debt = simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 1000000000, 25000000000)
}
err = suite.keeper.AddCdp(suite.ctx, addrs[j], cs(c(collateral, int64(amount))), cs(c("usdx", int64(debt))))
err = suite.keeper.AddCdp(suite.ctx, addrs[j], c(collateral, int64(amount)), c("usdx", int64(debt)))
suite.NoError(err)
c, f := suite.keeper.GetCDP(suite.ctx, collateral, uint64(j+1))
suite.True(f)
@ -113,7 +113,7 @@ 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)),
Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpParams(suite.cdps[0].Owner, suite.cdps[0].Collateral.Denom)),
}
bz, err := suite.querier(ctx, []string{types.QueryGetCdp}, query)
suite.Nil(err)
@ -154,7 +154,7 @@ 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)),
Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpsParams(suite.cdps[0].Collateral.Denom)),
}
bz, err := suite.querier(ctx, []string{types.QueryGetCdps}, query)
suite.Nil(err)
@ -181,9 +181,9 @@ func (suite *QuerierTestSuite) TestQueryCdpsByRatio() {
expectedBtcIds := []int{}
for _, cdp := range suite.cdps {
absoluteRatio := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdp.Collateral, cdp.Principal)
collateralizationRatio, err := suite.keeper.CalculateCollateralizationRatioFromAbsoluteRatio(suite.ctx, cdp.Collateral[0].Denom, absoluteRatio)
collateralizationRatio, err := suite.keeper.CalculateCollateralizationRatioFromAbsoluteRatio(suite.ctx, cdp.Collateral.Denom, absoluteRatio)
suite.Nil(err)
if cdp.Collateral[0].Denom == "xrp" {
if cdp.Collateral.Denom == "xrp" {
if collateralizationRatio.LT(xrpRatio) {
ratioCountXrp += 1
expectedXrpIds = append(expectedXrpIds, int(cdp.ID))
@ -262,7 +262,7 @@ func (suite *QuerierTestSuite) TestQueryDeposits() {
ctx := suite.ctx.WithIsCheckTx(false)
query := abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdpDeposits}, "/"),
Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpDeposits(suite.cdps[0].Owner, suite.cdps[0].Collateral[0].Denom)),
Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpDeposits(suite.cdps[0].Owner, suite.cdps[0].Collateral.Denom)),
}
bz, err := suite.querier(ctx, []string{types.QueryGetCdpDeposits}, query)

View File

@ -66,7 +66,7 @@ func (suite *SavingsTestSuite) TestGetSetPreviousDistributionTime() {
now := tmtime.Now()
_, f := suite.keeper.GetPreviousSavingsDistribution(suite.ctx)
suite.False(f)
suite.True(f)
suite.NotPanics(func() { suite.keeper.SetPreviousSavingsDistribution(suite.ctx, now) })

View File

@ -17,17 +17,11 @@ import (
// (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) error {
// Calculate the previous collateral ratio
oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees...))
oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
// Move debt coins from cdp to liquidator account
deposits := k.GetDeposits(ctx, cdp.ID)
debt := sdk.ZeroInt()
for _, pc := range cdp.Principal {
debt = debt.Add(pc.Amount)
}
for _, dc := range cdp.AccumulatedFees {
debt = debt.Add(dc.Amount)
}
debt := cdp.Principal.Amount.Add(cdp.AccumulatedFees.Amount)
modAccountDebt := k.getModAccountDebt(ctx, types.ModuleName)
if modAccountDebt.LT(debt) {
debt = modAccountDebt
@ -48,29 +42,24 @@ func (k Keeper) SeizeCollateral(ctx sdk.Context, cdp types.CDP) error {
sdk.NewAttribute(types.AttributeKeyDepositor, fmt.Sprintf("%s", dep.Depositor)),
),
)
err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.LiquidatorMacc, dep.Amount)
err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.LiquidatorMacc, sdk.NewCoins(dep.Amount))
if err != nil {
return err
}
k.DeleteDeposit(ctx, dep.CdpID, dep.Depositor)
}
err = k.AuctionCollateral(ctx, deposits, debt, cdp.Principal[0].Denom)
err = k.AuctionCollateral(ctx, deposits, debt, cdp.Principal.Denom)
if err != nil {
return 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)
}
coinsToDecrement := cdp.Principal.Add(cdp.AccumulatedFees)
k.DecrementTotalPrincipal(ctx, cdp.Collateral.Denom, coinsToDecrement)
// Delete CDP from state
k.RemoveCdpOwnerIndex(ctx, cdp)
k.RemoveCdpCollateralRatioIndex(ctx, cdp.Collateral[0].Denom, cdp.ID, oldCollateralToDebtRatio)
k.RemoveCdpCollateralRatioIndex(ctx, cdp.Collateral.Denom, cdp.ID, oldCollateralToDebtRatio)
k.DeleteCDP(ctx, cdp)
return nil
}

View File

@ -103,7 +103,7 @@ func (suite *SeizeTestSuite) createCdps() {
tracker.debt += int64(debt)
}
}
err := suite.keeper.AddCdp(suite.ctx, addrs[j], cs(c(collateral, int64(amount))), cs(c("usdx", int64(debt))))
err := suite.keeper.AddCdp(suite.ctx, addrs[j], c(collateral, int64(amount)), c("usdx", int64(debt)))
suite.NoError(err)
c, f := suite.keeper.GetCDP(suite.ctx, collateral, uint64(j+1))
suite.True(f)
@ -129,37 +129,40 @@ func (suite *SeizeTestSuite) setPrice(price sdk.Dec, market string) {
func (suite *SeizeTestSuite) TestSeizeCollateral() {
suite.createCdps()
sk := suite.app.GetSupplyKeeper()
cdp, _ := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2))
p := cdp.Principal[0].Amount
cl := cdp.Collateral[0].Amount
cdp, found := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2))
suite.True(found)
p := cdp.Principal.Amount
cl := cdp.Collateral.Amount
tpb := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx")
err := suite.keeper.SeizeCollateral(suite.ctx, cdp)
suite.NoError(err)
tpa := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx")
suite.Equal(tpb.Sub(tpa), p)
auctionKeeper := suite.app.GetAuctionKeeper()
_, found := auctionKeeper.GetAuction(suite.ctx, auction.DefaultNextAuctionID)
_, found = auctionKeeper.GetAuction(suite.ctx, auction.DefaultNextAuctionID)
suite.True(found)
auctionMacc := sk.GetModuleAccount(suite.ctx, auction.ModuleName)
suite.Equal(cs(c("debt", p.Int64()), c("xrp", cl.Int64())), auctionMacc.GetCoins())
ak := suite.app.GetAccountKeeper()
acc := ak.GetAccount(suite.ctx, suite.addrs[1])
suite.Equal(p.Int64(), acc.GetCoins().AmountOf("usdx").Int64())
err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[1], suite.addrs[1], cs(c("xrp", 10)))
err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[1], suite.addrs[1], c("xrp", 10))
suite.Require().True(errors.Is(err, types.ErrCdpNotFound))
}
func (suite *SeizeTestSuite) TestSeizeCollateralMultiDeposit() {
suite.createCdps()
sk := suite.app.GetSupplyKeeper()
cdp, _ := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2))
err := suite.keeper.DepositCollateral(suite.ctx, suite.addrs[1], suite.addrs[0], cs(c("xrp", 6999000000)))
cdp, found := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2))
suite.True(found)
err := suite.keeper.DepositCollateral(suite.ctx, suite.addrs[1], suite.addrs[0], c("xrp", 6999000000))
suite.NoError(err)
cdp, _ = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2))
cdp, found = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2))
suite.True(found)
deposits := suite.keeper.GetDeposits(suite.ctx, cdp.ID)
suite.Equal(2, len(deposits))
p := cdp.Principal[0].Amount
cl := cdp.Collateral[0].Amount
p := cdp.Principal.Amount
cl := cdp.Collateral.Amount
tpb := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx")
err = suite.keeper.SeizeCollateral(suite.ctx, cdp)
suite.NoError(err)
@ -170,7 +173,7 @@ func (suite *SeizeTestSuite) TestSeizeCollateralMultiDeposit() {
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)))
err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[1], suite.addrs[1], c("xrp", 10))
suite.Require().True(errors.Is(err, types.ErrCdpNotFound))
}
@ -180,7 +183,8 @@ func (suite *SeizeTestSuite) TestLiquidateCdps() {
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")
p, found := suite.keeper.GetCollateral(suite.ctx, "xrp")
suite.True(found)
suite.keeper.LiquidateCdps(suite.ctx, "xrp:usd", "xrp", p.LiquidationRatio)
acc = sk.GetModuleAccount(suite.ctx, types.ModuleName)
finalXrpCollateral := acc.GetCoins().AmountOf("xrp")

View File

@ -11,13 +11,13 @@ import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/cosmos/cosmos-sdk/x/auth"
sim "github.com/cosmos/cosmos-sdk/x/simulation"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/x/cdp/client/cli"
"github.com/kava-labs/kava/x/cdp/client/rest"
"github.com/kava-labs/kava/x/cdp/simulation"
"github.com/kava-labs/kava/x/cdp/types"
)
var (
@ -76,13 +76,13 @@ type AppModule struct {
AppModuleBasic
keeper Keeper
accountKeeper auth.AccountKeeper
pricefeedKeeper PricefeedKeeper
supplyKeeper SupplyKeeper
accountKeeper types.AccountKeeper
pricefeedKeeper types.PricefeedKeeper
supplyKeeper types.SupplyKeeper
}
// NewAppModule creates a new AppModule object
func NewAppModule(keeper Keeper, accountKeeper auth.AccountKeeper, pricefeedKeeper PricefeedKeeper, supplyKeeper SupplyKeeper) AppModule {
func NewAppModule(keeper Keeper, accountKeeper types.AccountKeeper, pricefeedKeeper types.PricefeedKeeper, supplyKeeper types.SupplyKeeper) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{},
keeper: keeper,

View File

@ -27,7 +27,7 @@ func TestDecodeDistributionStore(t *testing.T) {
cdpIds := []uint64{1, 2, 3, 4, 5}
denom := "denom"
oneCoins := sdk.NewCoins(sdk.NewCoin(denom, sdk.OneInt()))
oneCoins := sdk.NewCoin(denom, sdk.OneInt())
deposit := types.Deposit{CdpID: 1, Amount: oneCoins}
principal := sdk.OneInt()
prevDistTime := time.Now().UTC()

View File

@ -32,6 +32,7 @@ func RandomizedGenState(simState *module.SimulationState) {
sdk.NewCoin("xrp", sdk.NewInt(int64(simState.Rand.Intn(100000000000)))),
sdk.NewCoin("btc", sdk.NewInt(int64(simState.Rand.Intn(500000000)))),
sdk.NewCoin("usdx", sdk.NewInt(int64(simState.Rand.Intn(1000000000)))),
sdk.NewCoin("ukava", sdk.NewInt(int64(simState.Rand.Intn(500000000000)))),
)
err := acc.SetCoins(acc.GetCoins().Add(coinsToAdd...))
if err != nil {
@ -68,7 +69,7 @@ func randomCdpGenState(selection int) types.GenesisState {
case 0:
return types.GenesisState{
Params: types.Params{
GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 100000000000000)),
GlobalDebtLimit: sdk.NewInt64Coin("usdx", 100000000000000),
SurplusAuctionThreshold: types.DefaultSurplusThreshold,
DebtAuctionThreshold: types.DefaultDebtThreshold,
SavingsDistributionFrequency: types.DefaultSavingsDistributionFrequency,
@ -76,7 +77,7 @@ func randomCdpGenState(selection int) types.GenesisState {
{
Denom: "xrp",
LiquidationRatio: sdk.MustNewDecFromStr("2.0"),
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 20000000000000)),
DebtLimit: sdk.NewInt64Coin("usdx", 20000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000004431822130"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.075"),
AuctionSize: sdk.NewInt(100000000000),
@ -87,7 +88,7 @@ func randomCdpGenState(selection int) types.GenesisState {
{
Denom: "btc",
LiquidationRatio: sdk.MustNewDecFromStr("1.25"),
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 50000000000000)),
DebtLimit: sdk.NewInt64Coin("usdx", 50000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(1000000000),
@ -98,7 +99,7 @@ func randomCdpGenState(selection int) types.GenesisState {
{
Denom: "bnb",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 30000000000000)),
DebtLimit: sdk.NewInt64Coin("usdx", 30000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000002293273137"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.15"),
AuctionSize: sdk.NewInt(1000000000000),
@ -107,8 +108,7 @@ func randomCdpGenState(selection int) types.GenesisState {
ConversionFactor: sdk.NewInt(8),
},
},
DebtParams: types.DebtParams{
{
DebtParam: types.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
@ -116,7 +116,6 @@ func randomCdpGenState(selection int) types.GenesisState {
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
},
},
StartingCdpID: types.DefaultCdpStartingID,
DebtDenom: types.DefaultDebtDenom,
GovDenom: types.DefaultGovDenom,
@ -126,7 +125,7 @@ func randomCdpGenState(selection int) types.GenesisState {
case 1:
return types.GenesisState{
Params: types.Params{
GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 100000000000000)),
GlobalDebtLimit: sdk.NewInt64Coin("usdx", 100000000000000),
SurplusAuctionThreshold: types.DefaultSurplusThreshold,
DebtAuctionThreshold: types.DefaultDebtThreshold,
SavingsDistributionFrequency: types.DefaultSavingsDistributionFrequency,
@ -134,7 +133,7 @@ func randomCdpGenState(selection int) types.GenesisState {
{
Denom: "bnb",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 100000000000000)),
DebtLimit: sdk.NewInt64Coin("usdx", 100000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000002293273137"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.075"),
AuctionSize: sdk.NewInt(10000000000),
@ -143,8 +142,7 @@ func randomCdpGenState(selection int) types.GenesisState {
ConversionFactor: sdk.NewInt(8),
},
},
DebtParams: types.DebtParams{
{
DebtParam: types.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
@ -152,7 +150,6 @@ func randomCdpGenState(selection int) types.GenesisState {
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
},
},
StartingCdpID: types.DefaultCdpStartingID,
DebtDenom: types.DefaultDebtDenom,
GovDenom: types.DefaultGovDenom,

View File

@ -7,7 +7,6 @@ import (
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/simapp/helpers"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/simulation"
@ -23,7 +22,7 @@ const (
// WeightedOperations returns all the operations from the module with their respective weights
func WeightedOperations(
appParams simulation.AppParams, cdc *codec.Codec, ak auth.AccountKeeper,
appParams simulation.AppParams, cdc *codec.Codec, ak types.AccountKeeper,
k keeper.Keeper, pfk types.PricefeedKeeper,
) simulation.WeightedOperations {
var weightMsgCdp int
@ -43,7 +42,7 @@ func WeightedOperations(
}
// SimulateMsgCdp generates a MsgCreateCdp or MsgDepositCdp with random values.
func SimulateMsgCdp(ak auth.AccountKeeper, k keeper.Keeper, pfk types.PricefeedKeeper) simulation.Operation {
func SimulateMsgCdp(ak types.AccountKeeper, k keeper.Keeper, pfk types.PricefeedKeeper) simulation.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string,
) (simulation.OperationMsg, []simulation.FutureOperation, error) {
@ -61,8 +60,7 @@ func SimulateMsgCdp(ak auth.AccountKeeper, k keeper.Keeper, pfk types.PricefeedK
}
randCollateralParam := collateralParams[r.Intn(len(collateralParams))]
randDebtAsset := randCollateralParam.DebtLimit[r.Intn(len(randCollateralParam.DebtLimit))]
randDebtParam, _ := k.GetDebtParam(ctx, randDebtAsset.Denom)
debtParam, _ := k.GetDebtParam(ctx, randCollateralParam.DebtLimit.Denom)
if coins.AmountOf(randCollateralParam.Denom).IsZero() {
return simulation.NoOpMsg(types.ModuleName), nil, nil
}
@ -72,7 +70,7 @@ func SimulateMsgCdp(ak auth.AccountKeeper, k keeper.Keeper, pfk types.PricefeedK
return simulation.NoOpMsg(types.ModuleName), nil, err
}
// convert the price to the same units as the debt param
priceShifted := ShiftDec(price.Price, randDebtParam.ConversionFactor)
priceShifted := ShiftDec(price.Price, debtParam.ConversionFactor)
spendableCoins := acc.SpendableCoins(ctx.BlockTime())
fees, err := simulation.RandomFees(r, ctx, spendableCoins)
@ -85,7 +83,7 @@ func SimulateMsgCdp(ak auth.AccountKeeper, k keeper.Keeper, pfk types.PricefeedK
if !found {
// calculate the minimum amount of collateral that is needed to create a cdp with the debt floor amount of debt and the minimum liquidation ratio
// (debtFloor * liquidationRatio)/priceShifted
minCollateralDeposit := (sdk.NewDecFromInt(randDebtParam.DebtFloor).Mul(randCollateralParam.LiquidationRatio)).Quo(priceShifted)
minCollateralDeposit := (sdk.NewDecFromInt(debtParam.DebtFloor).Mul(randCollateralParam.LiquidationRatio)).Quo(priceShifted)
// convert to proper collateral units
minCollateralDeposit = ShiftDec(minCollateralDeposit, randCollateralParam.ConversionFactor)
// convert to integer and always round up
@ -104,17 +102,17 @@ func SimulateMsgCdp(ak auth.AccountKeeper, k keeper.Keeper, pfk types.PricefeedK
// calculate the max amount of debt that could be drawn for the chosen deposit
maxDebtDraw := collateralDepositValue.Quo(randCollateralParam.LiquidationRatio).TruncateInt()
// check that the debt limit hasn't been reached
availableAssetDebt := randCollateralParam.DebtLimit.AmountOf(randDebtParam.Denom).Sub(k.GetTotalPrincipal(ctx, randCollateralParam.Denom, randDebtParam.Denom))
if availableAssetDebt.LTE(randDebtParam.DebtFloor) {
availableAssetDebt := randCollateralParam.DebtLimit.Amount.Sub(k.GetTotalPrincipal(ctx, randCollateralParam.Denom, debtParam.Denom))
if availableAssetDebt.LTE(debtParam.DebtFloor) {
// debt limit has been reached
return simulation.NewOperationMsgBasic(types.ModuleName, "no-operation", "debt limit reached, cannot open cdp", false, nil), nil, nil
}
// ensure that the debt draw does not exceed the debt limit
maxDebtDraw = sdk.MinInt(maxDebtDraw, availableAssetDebt)
// randomly select a debt draw amount
debtDraw := sdk.NewInt(int64(simulation.RandIntBetween(r, int(randDebtParam.DebtFloor.Int64()), int(maxDebtDraw.Int64()))))
debtDraw := sdk.NewInt(int64(simulation.RandIntBetween(r, int(debtParam.DebtFloor.Int64()), int(maxDebtDraw.Int64()))))
msg := types.NewMsgCreateCDP(acc.GetAddress(), sdk.NewCoins(sdk.NewCoin(randCollateralParam.Denom, collateralDeposit)), sdk.NewCoins(sdk.NewCoin(randDebtParam.Denom, debtDraw)))
msg := types.NewMsgCreateCDP(acc.GetAddress(), sdk.NewCoin(randCollateralParam.Denom, collateralDeposit), sdk.NewCoin(debtParam.Denom, debtDraw))
tx := helpers.GenTx(
[]sdk.Msg{msg},
@ -126,19 +124,19 @@ func SimulateMsgCdp(ak auth.AccountKeeper, k keeper.Keeper, pfk types.PricefeedK
simAccount.PrivKey,
)
_, result, err := app.Deliver(tx)
_, _, err := app.Deliver(tx)
if err != nil {
return simulation.NoOpMsg(types.ModuleName), nil, err
}
return simulation.NewOperationMsg(msg, true, result.Log), nil, nil
return simulation.NewOperationMsg(msg, true, ""), nil, nil
}
// a cdp already exists, deposit to it, draw debt from it, or repay debt to it
// close 25% of the time
if canClose(acc, existingCDP, randDebtParam.Denom) && shouldClose(r) {
repaymentAmount := spendableCoins.AmountOf(randDebtParam.Denom)
msg := types.NewMsgRepayDebt(acc.GetAddress(), randCollateralParam.Denom, sdk.NewCoins(sdk.NewCoin(randDebtParam.Denom, repaymentAmount)))
if canClose(acc, existingCDP, debtParam.Denom) && shouldClose(r) {
repaymentAmount := spendableCoins.AmountOf(debtParam.Denom)
msg := types.NewMsgRepayDebt(acc.GetAddress(), randCollateralParam.Denom, sdk.NewCoin(debtParam.Denom, repaymentAmount))
tx := helpers.GenTx(
[]sdk.Msg{msg},
@ -150,18 +148,18 @@ func SimulateMsgCdp(ak auth.AccountKeeper, k keeper.Keeper, pfk types.PricefeedK
simAccount.PrivKey,
)
_, result, err := app.Deliver(tx)
_, _, err := app.Deliver(tx)
if err != nil {
return simulation.NoOpMsg(types.ModuleName), nil, err
}
return simulation.NewOperationMsg(msg, true, result.Log), nil, nil
return simulation.NewOperationMsg(msg, true, ""), nil, nil
}
// deposit 25% of the time
if hasCoins(acc, randCollateralParam.Denom) && shouldDeposit(r) {
randDepositAmount := sdk.NewInt(int64(simulation.RandIntBetween(r, 1, int(spendableCoins.AmountOf(randCollateralParam.Denom).Int64()))))
msg := types.NewMsgDeposit(acc.GetAddress(), acc.GetAddress(), sdk.NewCoins(sdk.NewCoin(randCollateralParam.Denom, randDepositAmount)))
msg := types.NewMsgDeposit(acc.GetAddress(), acc.GetAddress(), sdk.NewCoin(randCollateralParam.Denom, randDepositAmount))
tx := helpers.GenTx(
[]sdk.Msg{msg},
@ -173,22 +171,22 @@ func SimulateMsgCdp(ak auth.AccountKeeper, k keeper.Keeper, pfk types.PricefeedK
simAccount.PrivKey,
)
_, result, err := app.Deliver(tx)
_, _, err := app.Deliver(tx)
if err != nil {
return simulation.NoOpMsg(types.ModuleName), nil, err
}
return simulation.NewOperationMsg(msg, true, result.Log), nil, nil
return simulation.NewOperationMsg(msg, true, ""), nil, nil
}
// draw debt 25% of the time
if shouldDraw(r) {
collateralShifted := ShiftDec(sdk.NewDecFromInt(existingCDP.Collateral.AmountOf(randCollateralParam.Denom)), randCollateralParam.ConversionFactor.Neg())
collateralShifted := ShiftDec(sdk.NewDecFromInt(existingCDP.Collateral.Amount), randCollateralParam.ConversionFactor.Neg())
collateralValue := collateralShifted.Mul(priceShifted)
newFeesAccumulated := k.CalculateFees(ctx, existingCDP.Principal, sdk.NewInt(ctx.BlockTime().Unix()-existingCDP.FeesUpdated.Unix()), randCollateralParam.Denom).AmountOf(randDebtParam.Denom)
totalFees := existingCDP.AccumulatedFees.AmountOf(randCollateralParam.Denom).Add(newFeesAccumulated)
newFeesAccumulated := k.CalculateFees(ctx, existingCDP.Principal, sdk.NewInt(ctx.BlockTime().Unix()-existingCDP.FeesUpdated.Unix()), randCollateralParam.Denom).Amount
totalFees := existingCDP.AccumulatedFees.Amount.Add(newFeesAccumulated)
// given the current collateral value, calculate how much debt we could add while maintaining a valid liquidation ratio
debt := existingCDP.Principal.AmountOf(randDebtParam.Denom).Add(totalFees)
debt := existingCDP.Principal.Amount.Add(totalFees)
maxTotalDebt := collateralValue.Quo(randCollateralParam.LiquidationRatio)
maxDebt := (maxTotalDebt.Sub(sdk.NewDecFromInt(debt))).Mul(sdk.MustNewDecFromStr("0.95")).TruncateInt()
if maxDebt.LTE(sdk.OneInt()) {
@ -196,7 +194,7 @@ func SimulateMsgCdp(ak auth.AccountKeeper, k keeper.Keeper, pfk types.PricefeedK
return simulation.NewOperationMsgBasic(types.ModuleName, "no-operation", "cdp debt maxed out, cannot draw more debt", false, nil), nil, nil
}
// check if the debt limit has been reached
availableAssetDebt := randCollateralParam.DebtLimit.AmountOf(randDebtParam.Denom).Sub(k.GetTotalPrincipal(ctx, randCollateralParam.Denom, randDebtParam.Denom))
availableAssetDebt := randCollateralParam.DebtLimit.Amount.Sub(k.GetTotalPrincipal(ctx, randCollateralParam.Denom, debtParam.Denom))
if availableAssetDebt.LTE(sdk.OneInt()) {
// debt limit has been reached
return simulation.NewOperationMsgBasic(types.ModuleName, "no-operation", "debt limit reached, cannot draw more debt", false, nil), nil, nil
@ -204,7 +202,7 @@ func SimulateMsgCdp(ak auth.AccountKeeper, k keeper.Keeper, pfk types.PricefeedK
maxDraw := sdk.MinInt(maxDebt, availableAssetDebt)
randDrawAmount := sdk.NewInt(int64(simulation.RandIntBetween(r, 1, int(maxDraw.Int64()))))
msg := types.NewMsgDrawDebt(acc.GetAddress(), randCollateralParam.Denom, sdk.NewCoins(sdk.NewCoin(randDebtParam.Denom, randDrawAmount)))
msg := types.NewMsgDrawDebt(acc.GetAddress(), randCollateralParam.Denom, sdk.NewCoin(debtParam.Denom, randDrawAmount))
tx := helpers.GenTx(
[]sdk.Msg{msg},
@ -216,28 +214,29 @@ func SimulateMsgCdp(ak auth.AccountKeeper, k keeper.Keeper, pfk types.PricefeedK
simAccount.PrivKey,
)
_, result, err := app.Deliver(tx)
_, _, err := app.Deliver(tx)
if err != nil {
return simulation.NoOpMsg(types.ModuleName), nil, err
}
return simulation.NewOperationMsg(msg, true, result.Log), nil, nil
return simulation.NewOperationMsg(msg, true, ""), nil, nil
}
// repay debt 25% of the time
if hasCoins(acc, randDebtParam.Denom) {
debt := existingCDP.Principal.AmountOf(randDebtParam.Denom)
maxRepay := spendableCoins.AmountOf(randDebtParam.Denom)
payableDebt := debt.Sub(randDebtParam.DebtFloor)
if hasCoins(acc, debtParam.Denom) {
debt := existingCDP.Principal.Amount
maxRepay := spendableCoins.AmountOf(debtParam.Denom)
payableDebt := debt.Sub(debtParam.DebtFloor)
if maxRepay.GT(payableDebt) {
maxRepay = payableDebt
}
randRepayAmount := sdk.NewInt(int64(simulation.RandIntBetween(r, 1, int(maxRepay.Int64()))))
if debt.Equal(randDebtParam.DebtFloor) && spendableCoins.AmountOf(randDebtParam.Denom).GTE(debt) {
if debt.Equal(debtParam.DebtFloor) && spendableCoins.AmountOf(debtParam.Denom).GTE(debt) {
randRepayAmount = debt
}
msg := types.NewMsgRepayDebt(acc.GetAddress(), randCollateralParam.Denom, sdk.NewCoins(sdk.NewCoin(randDebtParam.Denom, randRepayAmount)))
msg := types.NewMsgRepayDebt(acc.GetAddress(), randCollateralParam.Denom, sdk.NewCoin(debtParam.Denom, randRepayAmount))
tx := helpers.GenTx(
[]sdk.Msg{msg},
@ -249,12 +248,12 @@ func SimulateMsgCdp(ak auth.AccountKeeper, k keeper.Keeper, pfk types.PricefeedK
simAccount.PrivKey,
)
_, result, err := app.Deliver(tx)
_, _, err := app.Deliver(tx)
if err != nil {
return simulation.NoOpMsg(types.ModuleName), nil, err
}
return simulation.NewOperationMsg(msg, true, result.Log), nil, nil
return simulation.NewOperationMsg(msg, true, ""), nil, nil
}
return simulation.NoOpMsg(types.ModuleName), nil, nil
@ -284,6 +283,6 @@ func shouldClose(r *rand.Rand) bool {
}
func canClose(acc authexported.Account, c types.CDP, denom string) bool {
repaymentAmount := c.Principal.Add(c.AccumulatedFees...).AmountOf(denom)
repaymentAmount := c.Principal.Add(c.AccumulatedFees).Amount
return acc.GetCoins().AmountOf(denom).GTE(repaymentAmount)
}

View File

@ -4,17 +4,30 @@
CDPs enable the creation of a stable asset by collateralization with another on chain asset.
A CDP is scoped to one collateral type. It has one primary owner, and a set of "depositors". The depositors can deposit and withdraw collateral to the CDP. The owner can draw stable assets (creating debt) and repay them to cancel the debt.
A CDP is scoped to one collateral type. It has one primary owner, and a set of "depositors". The depositors can deposit and withdraw collateral to the CDP. The owner can draw stable assets (creating debt), deposit and withdraw collateral, and repay stable assets to cancel the debt.
Once created stable assets are free to be transferred between users, but a CDP owner must repay their debt to get their collateral back.
Once created, stable assets are free to be transferred between users, but a CDP owner must repay their debt to get their collateral back.
User interactions with this module:
- create a new cdp by depositing some type of coin as collateral
- withdraw newly minted stable coin from this CDP (up to a fraction of the value of the collateral)
- create a new CDP by depositing a supported coin as collateral and minting debt
- deposit to a CDP controlled a different owner address
- withdraw deposited collateral, if it doesn't put the CDP below the liquidation ratio
- issue stable coins from this CDP (up to a fraction of the value of the collateral)
- repay debt by paying back stable coins (including paying any fees accrued)
- remove collateral and close CDP
Module interactions:
- fees for all CDPs are updated each block
- the value of fees (surplus) is divded between users, via the savings rate, and owners of the governance token, via burning governance tokens proportional to surplus
- the value of an asset that is supported for CDPs is determined by querying an external pricefeed
- if the price of an asset puts a CDP below the liquidation ratio, the CDP is liquidated
- liquidated collateral is divided into lots and sent to an external auction module
- collateral that is returned from the auction module is returned to the account that deposited that collateral
- if auctions do not recover the desired amount of debt, debt auctions are triggered after a certain threshold of global debt is reached
- surplus auctions are triggered after a certain threshold of surplus is triggered
## Liquidation & Stability System
In the event of a decrease in the price of the collateral, the total value of all collateral in CDPs may drop below the value of all the issued stable assets. This undesirable event is countered through two mechanisms:
@ -35,13 +48,13 @@ The cdp module uses two module accounts - one to hold debt coins associated with
When a user repays stable asset withdrawn from a CDP, they must also pay a fee.
This is calculated according to the amount of stable asset withdrawn and the time withdrawn for. Like interest on a loan fees grow at a compounding percentage of original debt.
This is calculated according to the amount of stable asset withdrawn and the time withdrawn for. Like interest on a loan, fees grow at a compounding percentage of original debt.
Fees create incentives to open or close CDPs and can be changed by governance to help keep the system functioning through changing market conditions.
A further fee is applied on liquidation of a CDP. Normally when the collateral is sold to cover the debt, any excess not sold is returned to the CDP holder. The liquidation fee reduces the amount of excess collateral returned, representing a cut that the system takes.
Fees accumulate to the system before being automatically sold at auction for governance token. These are then burned, acting as incentive for safe governance of the system.
Fees accumulate to the system and are split between the savings rate and surplus. Fees accumulated by the savings rate are distributed directly to holders of stable coins at a specified frequency. Savings rate distributions are proportional to tokens held. For example, if an account holds 1% of all stable coins, they will receive 1% of the savings rate distribution. Fees accumulated as surplus are automatically sold at auction for governance token once a certain threshold is reached. The governance tokens raised at auction are then burned, acting as incentive for safe governance of the system.
## Governance
@ -52,6 +65,7 @@ Governance is important for actions such as:
- enabling CDPs to be created with new collateral assets
- changing fee rates to incentivize behavior
- increasing the debt ceiling to allow more stable asset to be created
- increasing/decreasing the savings rate to promote stability of the debt asset
## Dependency: supply

View File

@ -22,16 +22,17 @@ The CDP's collateral always equal to the total of the deposits.
type CDP struct {
ID uint64
Owner sdk.AccAddress
Collateral sdk.Coins
Principal sdk.Coins
AccumulatedFees sdk.Coins
Collateral sdk.Coin
Principal sdk.Coin
AccumulatedFees sdk.Coin
FeesUpdated time.Time
}
```
CDPs are stored with a couple of database indexes for faster lookup:
CDPs are stored with three database indexes for faster lookup:
- by collateral ratio - to look up cdps that are close to the liquidation ratio
- by collateral denom - to look up cdps with a particular collateral asset
- by owner index - to look up cdps that an address is the owner of
## Deposit
@ -42,7 +43,7 @@ A Deposit is a struct recording collateral added to a CDP by one address. The ad
type Deposit struct {
CdpID uint64
Depositor sdk.AccAddress
Amount sdk.Coins
Amount sdk.Coin
}
```
@ -58,10 +59,14 @@ A global counter used to create unique CDP ids.
The name of the internal debt coin. Its value can be configured at genesis.
## GovDenom
The name of the internal governance coin. Its value can be configured at genesis.
## Total Principle
Sum of all non seized debt plus accumulated fees. This is used to calculate the new debt created every block due to the fee interest rate.
Sum of all non seized debt plus accumulated fees.
## Previous Block Time
## Previous Savings Distribution Time
A record of the last block time used to calculate fees.
A record of the last block time when the savings rate was distributed

View File

@ -9,8 +9,8 @@ CreateCDP sets up and stores a new CDP, adding collateral from the sender, and d
```go
type MsgCreateCDP struct {
Sender sdk.AccAddress
Collateral sdk.Coins
Principal sdk.Coins
Collateral sdk.Coin
Principal sdk.Coin
}
```
@ -29,7 +29,7 @@ Deposit adds collateral to a CDP in the form of a deposit. Collateral is taken f
type MsgDeposit struct {
Owner sdk.AccAddress
Depositor sdk.AccAddress
Collateral sdk.Coins
Collateral sdk.Coin
}
```
@ -47,15 +47,14 @@ Withdraw removes collateral from a CDP, provided it would not put the CDP under
type MsgWithdraw struct {
Owner sdk.AccAddress
Depositor sdk.AccAddress
Collateral sdk.Coins
Collateral sdk.Coin
}
```
State Changes:
- `Collateral` coins are sent from the cdp module account to `Depositor`
- `Collateral` amount of coins subtracted from the `Deposit` struct
- cdp fees are updated (see below)
- `Collateral` amount of coins subtracted from the `Deposit` struct. If the amount is now zero, the struct is deleted
## DrawDebt
@ -65,7 +64,7 @@ DrawDebt creates debt in a CDP, minting new stable asset which is sent to the se
type MsgDrawDebt struct {
Sender sdk.AccAddress
CdpDenom string
Principal sdk.Coins
Principal sdk.Coin
}
```
@ -74,7 +73,6 @@ State Changes:
- mint `Principal` coins and send them to `Sender`, updating the CDP's `Principal` field
- mint equal amount of internal debt coins and store in the module account
- increment total principal for principal denom
- cdp fees are updated (see below)
## RepayDebt
@ -84,7 +82,7 @@ RepayDebt removes some debt from a CDP and burns the corresponding amount of sta
type MsgRepayDebt struct {
Sender sdk.AccAddress
CdpDenom string
Payment sdk.Coins
Payment sdk.Coin
}
```
@ -93,13 +91,12 @@ State Changes:
- burn `Payment` coins taken from `Sender`, updating the CDP by reducing `Principal` field by `Paymment`
- burn an equal amount of internal debt coins
- decrement total principal for payment denom
- cdp fees are updated (see below)
- if fees and principal are zero, return collateral to depositors:
- if fees and principal are zero, return collateral to depositors and delete the CDP struct:
- For each deposit, send coins from the cdp module account to the depositor, and delete the deposit struct from store.
## Fees
When CDPs are updated by the above messages the fees accumulated since the last update are calculated and added on.
At the beginning of each block, fees accumulated since the last update are calculated and added on.
```
feesAccumulated = (outstandingDebt * (feeRate^periods)) - outstandingDebt
@ -111,6 +108,10 @@ where:
- `periods` is the number of seconds since last fee update
- `feeRate` is the per second debt interest rate
Fees are divided between surplus and savings rate. For example, if the savings rate is 0.95, 95% of all fees go towards the savings rate and 5% go to surplus.
In the event that the rounded value of `feesAccumulated` is zero, fees are not updated, and the `FeesUpdated` value on the CDP struct is not updated. When a sufficient number of periods have passed such that the rounded value is no longer zero, fees will be updated.
## Database Indexes
When CDPs are update by the above messages the database indexes are also updated.

View File

@ -2,30 +2,26 @@
At the start of every block the BeginBlocker of the cdp module:
- updates total CDP fees
- update fees for individual "risky" CDPs
- updates fees for CDPs
- liquidates CDPs under the collateral ratio
- nets out system debt and, if necessary, starts auctions to re-balance it
- records the last block time
- pays out the savings rate if sufficient time has past
- records the last savings rate distribution, if one occurred
## Update Fees
- The total fees accumulated since the last block across all CDPs are calculated.
- The total fees accumulated since the last block for each CDP are calculated.
- If the fee amount is non-zero:
- Set the updated value for fees
- Set the fees updated time for the CDP to the current block time
- An equal amount of debt coins are minted and sent to the system's CDP module account.
- An equal amount of stable asset coins are minted and sent to the system's liquidator module account
## Update risky cdps
- UpdateFeesForRiskyCdps calculates fees for risky CDPs
- Select the CDPs with 10% of the liquidation ratio - the risky CDPs
- Calculate additional accumulated fees on each of those CDPs
- Update the fees updated time for the CDP to the current block time
- Increment total principal.
## Liquidate CDP
- Get every cdp that is under the liquidation ratio for its collateral type.
- For each cdp:
- Calculate and update fees since last update.
- Remove all collateral and internal debt coins from cdp and deposits and delete it. Send the coins to the liquidator module account.
- Start auctions of a fixed size from this collateral (with any remainder in a smaller sized auction), sending collateral and debt coins to the auction module account.
- Decrement total principal.
@ -42,7 +38,3 @@ At the start of every block the BeginBlocker of the cdp module:
- If `SavingsDistributionFrequency` seconds have elapsed since the previous distribution, the savings rate is applied to all accounts that hold stable asset.
- Each account that holds stable asset is distributed a ratable portion of the surplus that is apportioned to the savings rate.
- If distribution occurred, the time of the distribution is recorded.
## Update Previous Block Time
The current block time is recorded.

View File

@ -3,10 +3,10 @@
The cdp module contains the following parameters:
| Key | Type | Example | Description |
|------------------ |-------------------------|------------------------------------|------------------------------------------------------------------|
|------------------------------|-------------------------|------------------------------------|------------------------------------------------------------------|
| CollateralParams | array (CollateralParam) | [{see below}] | array of params for each enabled collateral type |
| DebtParams | array (DebtParam) | [{see below}] | array of params for each enabled pegged asset |
| GlobalDebtLimit | array (coin) | [{"denom":"usdx","amount":"1000"}] | maximum pegged assets that can be minted across the whole system |
| DebtParams | DebtParam | {see below} | array of params for each enabled pegged asset |
| GlobalDebtLimit | coin | {"denom":"usdx","amount":"1000"} | maximum pegged assets that can be minted across the whole system |
| SavingsDistributionFrequency | string (int) | "84600" | number of seconds between distribution of the savings rate |
| CircuitBreaker | bool | false | flag to disable user interactions with the system |
@ -14,15 +14,15 @@ Each CollateralParam has the following parameters:
| Key | Type | Example | Description |
|------------------|---------------|---------------------------------------------|----------------------------------------------------------------------------------------------------------------|
| Denom | string | "pbnb" | collateral coin denom |
| Denom | string | "bnb" | collateral coin denom |
| LiquidationRatio | string (dec) | "1.500000000000000000" | the ratio under which a cdp with this collateral type will be liquidated |
| DebtLimit | array (coin) | [{"denom":"pbnb","amount":"1000000000000"}] | maximum pegged asset that can be minted backed by this collateral type |
| DebtLimit | coin | {"denom":"bnb","amount":"1000000000000"} | maximum pegged asset that can be minted backed by this collateral type |
| StabilityFee | string (dec) | "1.000000001547126" | per second fee |
| Prefix | number (byte) | 34 | identifier used in store keys - **must** be unique across collateral types |
| MarketID | string | "BNB/USD" | price feed identifier for this collateral type |
| MarketID | string | "bnb:usd" | price feed identifier for this collateral type |
| ConversionFactor | string (int) | "6" | 10^_ multiplier to go from external amount (say BTC1.50) to internal representation of that amount (150000000) |
Each DebtParam has the following parameters:
DebtParam has the following parameters:
| Key | Type | Example | Description |
|------------------|--------------|------------|------------------------------------------------------------------------------------------------------------|
@ -30,3 +30,4 @@ Each DebtParam has the following parameters:
| ReferenceAsset | string | "USD" | asset this asset is pegged to, informational purposes only |
| ConversionFactor | string (int) | "6" | 10^_ multiplier to go from external amount (say $1.50) to internal representation of that amount (1500000) |
| DebtFloor | string (int) | "10000000" | minimum amount of debt that a CDP can contain |
| SavingsRate | string (dec) | "0.95" | the percentage of accumulated fees that go towards the savings rate |

View File

@ -16,4 +16,4 @@ The `x/cdp` module stores and manages Collateralized Debt Positions (or CDPs).
A CDP enables the creation of a stable asset pegged to an external price (usually US Dollar) by collateralization with another asset. Collateral is locked in a CDP and new stable asset can be minted up to some fraction of the value of the collateral. To unlock the collateral, the debt must be repaid by returning some stable asset to the CDP at which point it will be burned and the collateral unlocked.
Pegged assets remain fully collateralized by the value locked in CDPs. In the event of price changes, this collateral can be seized and sold off by the system to reclaim and reduce the supply of stable assets.
Pegged assets remain fully collateralized by the value locked in CDPs. In the event of price changes, this collateral can be seized and sold off in auctions by the system to reclaim and reduce the supply of stable assets.

View File

@ -12,15 +12,15 @@ import (
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"`
Collateral sdk.Coin `json:"collateral" yaml:"collateral"` // Amount of collateral stored in this CDP
Principal sdk.Coin `json:"principal" yaml:"principal"`
AccumulatedFees sdk.Coin `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
func NewCDP(id uint64, owner sdk.AccAddress, collateral sdk.Coin, principal sdk.Coin, time time.Time) CDP {
fees := sdk.NewCoin(principal.Denom, sdk.ZeroInt())
return CDP{
ID: id,
Owner: owner,
@ -43,7 +43,7 @@ func (cdp CDP) String() string {
Fees Last Updated: %s`,
cdp.Owner,
cdp.ID,
cdp.Collateral[0].Denom,
cdp.Collateral.Denom,
cdp.Collateral,
cdp.Principal,
cdp.AccumulatedFees,
@ -101,7 +101,7 @@ func (augCDP AugmentedCDP) String() string {
Collateralization ratio: %s`,
augCDP.Owner,
augCDP.ID,
augCDP.Collateral[0].Denom,
augCDP.Collateral.Denom,
augCDP.Collateral,
augCDP.CollateralValue,
augCDP.Principal,

View File

@ -10,11 +10,11 @@ import (
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
Amount sdk.Coin `json:"amount" yaml:"amount"` // Deposit amount
}
// NewDeposit creates a new Deposit object
func NewDeposit(cdpID uint64, depositor sdk.AccAddress, amount sdk.Coins) Deposit {
func NewDeposit(cdpID uint64, depositor sdk.AccAddress, amount sdk.Coin) Deposit {
return Deposit{cdpID, depositor, amount}
}
@ -56,7 +56,7 @@ func (ds Deposits) SumCollateral() (sum sdk.Int) {
sum = sdk.ZeroInt()
for _, d := range ds {
if !d.Amount.IsZero() {
sum = sum.Add(d.Amount[0].Amount)
sum = sum.Add(d.Amount.Amount)
}
}
return

View File

@ -25,18 +25,20 @@ var (
ErrDepositNotFound = sdkerrors.Register(ModuleName, 9, "deposit not found")
// ErrInvalidDeposit error for invalid deposit
ErrInvalidDeposit = sdkerrors.Register(ModuleName, 10, "invalid deposit")
// ErrInvalidCollateral error for invalid collateral
ErrInvalidCollateral = sdkerrors.Register(ModuleName, 11, "collateral not supported")
// ErrInvalidPayment error for invalid payment
ErrInvalidPayment = sdkerrors.Register(ModuleName, 12, "invalid payment")
ErrInvalidPayment = sdkerrors.Register(ModuleName, 11, "invalid payment")
//ErrDepositNotAvailable error for withdrawing deposits in liquidation
ErrDepositNotAvailable = sdkerrors.Register(ModuleName, 13, "deposit in liquidation")
ErrDepositNotAvailable = sdkerrors.Register(ModuleName, 12, "deposit in liquidation")
// ErrInvalidWithdrawAmount error for invalid withdrawal amount
ErrInvalidWithdrawAmount = sdkerrors.Register(ModuleName, 14, "withdrawal amount exceeds deposit")
ErrInvalidWithdrawAmount = sdkerrors.Register(ModuleName, 13, "withdrawal amount exceeds deposit")
//ErrCdpNotAvailable error for depositing to a CDP in liquidation
ErrCdpNotAvailable = sdkerrors.Register(ModuleName, 15, "cannot modify cdp in liquidation")
ErrCdpNotAvailable = sdkerrors.Register(ModuleName, 14, "cannot modify cdp in liquidation")
// ErrBelowDebtFloor error for creating a cdp with debt below the minimum
ErrBelowDebtFloor = sdkerrors.Register(ModuleName, 16, "proposed cdp debt is below minimum")
ErrBelowDebtFloor = sdkerrors.Register(ModuleName, 15, "proposed cdp debt is below minimum")
// ErrLoadingAugmentedCDP error loading augmented cdp
ErrLoadingAugmentedCDP = sdkerrors.Register(ModuleName, 17, "augmented cdp could not be loaded from cdp")
ErrLoadingAugmentedCDP = sdkerrors.Register(ModuleName, 16, "augmented cdp could not be loaded from cdp")
// ErrInvalidDebtRequest error for invalid principal input length
ErrInvalidDebtRequest = sdkerrors.Register(ModuleName, 17, "only one principal type per cdp")
// ErrDenomPrefixNotFound error for denom prefix not found
ErrDenomPrefixNotFound = sdkerrors.Register(ModuleName, 18, "denom prefix not found")
)

View File

@ -9,7 +9,7 @@ import (
pftypes "github.com/kava-labs/kava/x/pricefeed/types"
)
// SupplyKeeper defines the expected supply keeper for module accounts
// SupplyKeeper defines the expected supply keeper for module accounts (noalias)
type SupplyKeeper interface {
GetModuleAddress(name string) sdk.AccAddress
GetModuleAccount(ctx sdk.Context, name string) supplyexported.ModuleAccountI
@ -25,7 +25,7 @@ type SupplyKeeper interface {
GetSupply(ctx sdk.Context) (supply supplyexported.SupplyI)
}
// PricefeedKeeper defines the expected interface for the pricefeed
// PricefeedKeeper defines the expected interface for the pricefeed (noalias)
type PricefeedKeeper interface {
GetCurrentPrice(sdk.Context, string) (pftypes.CurrentPrice, error)
GetParams(sdk.Context) pftypes.Params
@ -45,4 +45,5 @@ type AuctionKeeper interface {
// AccountKeeper expected interface for the account keeper (noalias)
type AccountKeeper interface {
IterateAccounts(ctx sdk.Context, cb func(account authexported.Account) (stop bool))
GetAccount(ctx sdk.Context, addr sdk.AccAddress) authexported.Account
}

View File

@ -4,6 +4,8 @@ import (
"bytes"
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// GenesisState is the state that must be provided at genesis.
@ -55,9 +57,12 @@ func (gs GenesisState) Validate() error {
return fmt.Errorf("previous distribution time not set")
}
if gs.DebtDenom == "" {
return fmt.Errorf("debt denom not set")
if err := sdk.ValidateDenom(gs.DebtDenom); err != nil {
return fmt.Errorf(fmt.Sprintf("debt denom invalid: %v", err))
}
if err := sdk.ValidateDenom(gs.GovDenom); err != nil {
return fmt.Errorf(fmt.Sprintf("gov denom invalid: %v", err))
}
return nil

View File

@ -21,12 +21,12 @@ var (
// 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"`
Collateral sdk.Coin `json:"collateral" yaml:"collateral"`
Principal sdk.Coin `json:"principal" yaml:"principal"`
}
// NewMsgCreateCDP returns a new MsgPlaceBid.
func NewMsgCreateCDP(sender sdk.AccAddress, collateral sdk.Coins, principal sdk.Coins) MsgCreateCDP {
func NewMsgCreateCDP(sender sdk.AccAddress, collateral sdk.Coin, principal sdk.Coin) MsgCreateCDP {
return MsgCreateCDP{
Sender: sender,
Collateral: collateral,
@ -45,13 +45,10 @@ func (msg MsgCreateCDP) ValidateBasic() error {
if msg.Sender.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
if msg.Collateral.Len() != 1 {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "cdps do not support multiple collateral types: %s", msg.Collateral)
}
if !msg.Collateral.IsValid() {
if msg.Collateral.IsZero() || !msg.Collateral.IsValid() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "collateral amount %s", msg.Collateral)
}
if msg.Principal.Empty() || !msg.Principal.IsValid() {
if msg.Principal.IsZero() || !msg.Principal.IsValid() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "principal amount %s", msg.Principal)
}
return nil
@ -81,11 +78,11 @@ func (msg MsgCreateCDP) String() string {
type MsgDeposit struct {
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
Collateral sdk.Coins `json:"collateral" yaml:"collateral"`
Collateral sdk.Coin `json:"collateral" yaml:"collateral"`
}
// NewMsgDeposit returns a new MsgDeposit
func NewMsgDeposit(owner sdk.AccAddress, depositor sdk.AccAddress, collateral sdk.Coins) MsgDeposit {
func NewMsgDeposit(owner sdk.AccAddress, depositor sdk.AccAddress, collateral sdk.Coin) MsgDeposit {
return MsgDeposit{
Owner: owner,
Depositor: depositor,
@ -107,10 +104,7 @@ func (msg MsgDeposit) ValidateBasic() error {
if msg.Depositor.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
if msg.Collateral.Len() != 1 {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "cdps do not support multiple collateral types: %s", msg.Collateral)
}
if !msg.Collateral.IsValid() {
if !msg.Collateral.IsValid() || msg.Collateral.IsZero() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "collateral amount %s", msg.Collateral)
}
return nil
@ -140,11 +134,11 @@ func (msg MsgDeposit) String() string {
type MsgWithdraw struct {
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
Collateral sdk.Coins `json:"collateral" yaml:"collateral"`
Collateral sdk.Coin `json:"collateral" yaml:"collateral"`
}
// NewMsgWithdraw returns a new MsgDeposit
func NewMsgWithdraw(owner sdk.AccAddress, depositor sdk.AccAddress, collateral sdk.Coins) MsgWithdraw {
func NewMsgWithdraw(owner sdk.AccAddress, depositor sdk.AccAddress, collateral sdk.Coin) MsgWithdraw {
return MsgWithdraw{
Owner: owner,
Depositor: depositor,
@ -166,10 +160,7 @@ func (msg MsgWithdraw) ValidateBasic() error {
if msg.Depositor.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
if msg.Collateral.Len() != 1 {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "cdps do not support multiple collateral types: %s", msg.Collateral)
}
if !msg.Collateral.IsValid() {
if !msg.Collateral.IsValid() || msg.Collateral.IsZero() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "collateral amount %s", msg.Collateral)
}
return nil
@ -195,15 +186,15 @@ func (msg MsgWithdraw) String() string {
`, msg.Owner, msg.Depositor, msg.Collateral)
}
// MsgDrawDebt draw coins off of collateral in cdp
// MsgDrawDebt draw debt 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"`
Principal sdk.Coin `json:"principal" yaml:"principal"`
}
// NewMsgDrawDebt returns a new MsgDrawDebt
func NewMsgDrawDebt(sender sdk.AccAddress, denom string, principal sdk.Coins) MsgDrawDebt {
func NewMsgDrawDebt(sender sdk.AccAddress, denom string, principal sdk.Coin) MsgDrawDebt {
return MsgDrawDebt{
Sender: sender,
CdpDenom: denom,
@ -225,7 +216,7 @@ func (msg MsgDrawDebt) ValidateBasic() error {
if strings.TrimSpace(msg.CdpDenom) == "" {
return errors.New("cdp denom cannot be blank")
}
if msg.Principal.Empty() || !msg.Principal.IsValid() {
if msg.Principal.IsZero() || !msg.Principal.IsValid() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "principal amount %s", msg.Principal)
}
return nil
@ -255,11 +246,11 @@ func (msg MsgDrawDebt) String() string {
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"`
Payment sdk.Coin `json:"payment" yaml:"payment"`
}
// NewMsgRepayDebt returns a new MsgRepayDebt
func NewMsgRepayDebt(sender sdk.AccAddress, denom string, payment sdk.Coins) MsgRepayDebt {
func NewMsgRepayDebt(sender sdk.AccAddress, denom string, payment sdk.Coin) MsgRepayDebt {
return MsgRepayDebt{
Sender: sender,
CdpDenom: denom,
@ -281,7 +272,7 @@ func (msg MsgRepayDebt) ValidateBasic() error {
if strings.TrimSpace(msg.CdpDenom) == "" {
return errors.New("cdp denom cannot be blank")
}
if msg.Payment.Empty() || !msg.Payment.IsValid() {
if msg.Payment.IsZero() || !msg.Payment.IsValid() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "payment amount %s", msg.Payment)
}
return nil

View File

@ -8,9 +8,8 @@ import (
)
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()
coinsSingle = sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000)
coinsZero = sdk.NewCoin(sdk.DefaultBondDenom, sdk.ZeroInt())
addrs = []sdk.AccAddress{
sdk.AccAddress("test1"),
sdk.AccAddress("test2"),
@ -21,15 +20,13 @@ func TestMsgCreateCDP(t *testing.T) {
tests := []struct {
description string
sender sdk.AccAddress
collateral sdk.Coins
principal sdk.Coins
collateral sdk.Coin
principal sdk.Coin
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},
}
@ -52,13 +49,12 @@ func TestMsgDeposit(t *testing.T) {
description string
sender sdk.AccAddress
depositor sdk.AccAddress
collateral sdk.Coins
collateral sdk.Coin
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},
}
@ -82,13 +78,12 @@ func TestMsgWithdraw(t *testing.T) {
description string
sender sdk.AccAddress
depositor sdk.AccAddress
collateral sdk.Coins
collateral sdk.Coin
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},
}
@ -112,12 +107,11 @@ func TestMsgDrawDebt(t *testing.T) {
description string
sender sdk.AccAddress
denom string
principal sdk.Coins
principal sdk.Coin
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},
}
@ -141,12 +135,11 @@ func TestMsgRepayDebt(t *testing.T) {
description string
sender sdk.AccAddress
denom string
payment sdk.Coins
payment sdk.Coin
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},
}

View File

@ -15,31 +15,39 @@ import (
var (
KeyGlobalDebtLimit = []byte("GlobalDebtLimit")
KeyCollateralParams = []byte("CollateralParams")
KeyDebtParams = []byte("DebtParams")
KeyDebtParam = []byte("DebtParam")
KeyDistributionFrequency = []byte("DistributionFrequency")
KeyCircuitBreaker = []byte("CircuitBreaker")
KeyDebtThreshold = []byte("DebtThreshold")
KeySurplusThreshold = []byte("SurplusThreshold")
DefaultGlobalDebt = sdk.Coins{}
DefaultGlobalDebt = sdk.NewCoin(DefaultStableDenom, sdk.ZeroInt())
DefaultCircuitBreaker = false
DefaultCollateralParams = CollateralParams{}
DefaultDebtParams = DebtParams{}
DefaultDebtParam = DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
}
DefaultCdpStartingID = uint64(1)
DefaultDebtDenom = "debt"
DefaultGovDenom = "ukava"
DefaultStableDenom = "usdx"
DefaultSurplusThreshold = sdk.NewInt(1000000000)
DefaultDebtThreshold = sdk.NewInt(1000000000)
DefaultPreviousDistributionTime = tmtime.Canonical(time.Unix(0, 0))
DefaultSavingsDistributionFrequency = time.Hour * 24 * 2
DefaultSavingsDistributionFrequency = time.Hour * 12
minCollateralPrefix = 0
maxCollateralPrefix = 255
stabilityFeeMax = sdk.MustNewDecFromStr("1.000000051034942716") // 500% APR
)
// 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"`
DebtParam DebtParam `json:"debt_param" yaml:"debt_param"`
GlobalDebtLimit sdk.Coin `json:"global_debt_limit" yaml:"global_debt_limit"`
SurplusAuctionThreshold sdk.Int `json:"surplus_auction_threshold" yaml:"surplus_auction_threshold"`
DebtAuctionThreshold sdk.Int `json:"debt_auction_threshold" yaml:"debt_auction_threshold"`
SavingsDistributionFrequency time.Duration `json:"savings_distribution_frequency" yaml:"savings_distribution_frequency"`
@ -56,16 +64,16 @@ func (p Params) String() string {
Debt Auction Threshold: %s
Savings Distribution Frequency: %s
Circuit Breaker: %t`,
p.GlobalDebtLimit, p.CollateralParams, p.DebtParams, p.SurplusAuctionThreshold, p.DebtAuctionThreshold, p.SavingsDistributionFrequency, p.CircuitBreaker,
p.GlobalDebtLimit, p.CollateralParams, p.DebtParam, p.SurplusAuctionThreshold, p.DebtAuctionThreshold, p.SavingsDistributionFrequency, p.CircuitBreaker,
)
}
// NewParams returns a new params object
func NewParams(debtLimit sdk.Coins, collateralParams CollateralParams, debtParams DebtParams, surplusThreshold sdk.Int, debtThreshold sdk.Int, distributionFreq time.Duration, breaker bool) Params {
func NewParams(debtLimit sdk.Coin, collateralParams CollateralParams, debtParam DebtParam, surplusThreshold sdk.Int, debtThreshold sdk.Int, distributionFreq time.Duration, breaker bool) Params {
return Params{
GlobalDebtLimit: debtLimit,
CollateralParams: collateralParams,
DebtParams: debtParams,
DebtParam: debtParam,
DebtAuctionThreshold: debtThreshold,
SurplusAuctionThreshold: surplusThreshold,
SavingsDistributionFrequency: distributionFreq,
@ -75,14 +83,14 @@ func NewParams(debtLimit sdk.Coins, collateralParams CollateralParams, debtParam
// DefaultParams returns default params for cdp module
func DefaultParams() Params {
return NewParams(DefaultGlobalDebt, DefaultCollateralParams, DefaultDebtParams, DefaultSurplusThreshold, DefaultDebtThreshold, DefaultSavingsDistributionFrequency, DefaultCircuitBreaker)
return NewParams(DefaultGlobalDebt, DefaultCollateralParams, DefaultDebtParam, DefaultSurplusThreshold, DefaultDebtThreshold, DefaultSavingsDistributionFrequency, 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
DebtLimit sdk.Coin `json:"debt_limit" yaml:"debt_limit"` // Maximum amount of debt allowed to be drawn from this collateral type
StabilityFee sdk.Dec `json:"stability_fee" yaml:"stability_fee"` // per second stability fee for loans opened using this collateral
AuctionSize sdk.Int `json:"auction_size" yaml:"auction_size"` // Max amount of collateral to sell off in any one auction.
LiquidationPenalty sdk.Dec `json:"liquidation_penalty" yaml:"liquidation_penalty"` // percentage penalty (between [0, 1]) applied to a cdp if it is liquidated
@ -132,7 +140,9 @@ func (dp DebtParam) String() string {
Denom: %s
Reference Asset: %s
Conversion Factor: %s
Debt Floor %s`, dp.Denom, dp.ReferenceAsset, dp.ConversionFactor, dp.DebtFloor)
Debt Floor %s
Savings Rate %s
`, dp.Denom, dp.ReferenceAsset, dp.ConversionFactor, dp.DebtFloor, dp.SavingsRate)
}
// DebtParams array of DebtParam
@ -159,7 +169,7 @@ func (p *Params) ParamSetPairs() params.ParamSetPairs {
return params.ParamSetPairs{
params.NewParamSetPair(KeyGlobalDebtLimit, &p.GlobalDebtLimit, validateGlobalDebtLimitParam),
params.NewParamSetPair(KeyCollateralParams, &p.CollateralParams, validateCollateralParams),
params.NewParamSetPair(KeyDebtParams, &p.DebtParams, validateDebtParams),
params.NewParamSetPair(KeyDebtParam, &p.DebtParam, validateDebtParam),
params.NewParamSetPair(KeyCircuitBreaker, &p.CircuitBreaker, validateCircuitBreakerParam),
params.NewParamSetPair(KeySurplusThreshold, &p.SurplusAuctionThreshold, validateSurplusAuctionThresholdParam),
params.NewParamSetPair(KeyDebtThreshold, &p.DebtAuctionThreshold, validateDebtAuctionThresholdParam),
@ -177,7 +187,7 @@ func (p Params) Validate() error {
return err
}
if err := validateDebtParams(p.DebtParams); err != nil {
if err := validateDebtParam(p.DebtParam); err != nil {
return err
}
@ -197,39 +207,50 @@ func (p Params) Validate() error {
return err
}
debtDenoms := make(map[string]bool)
for _, dp := range p.DebtParams {
debtDenoms[dp.Denom] = true
if len(p.CollateralParams) == 0 { // default value OK
return nil
}
if (DebtParam{}) != p.DebtParam {
if p.DebtParam.Denom != p.GlobalDebtLimit.Denom {
return fmt.Errorf("debt denom %s does not match global debt denom %s",
p.DebtParam.Denom, p.GlobalDebtLimit.Denom)
}
}
// validate collateral params
collateralDupMap := make(map[string]int)
prefixDupMap := make(map[int]int)
collateralParamsDebtLimit := sdk.Coins{}
collateralParamsDebtLimit := sdk.ZeroInt()
for _, cp := range p.CollateralParams {
prefix := int(cp.Prefix)
prefixDupMap[prefix] = 1
collateralDupMap[cp.Denom] = 1
collateralParamsDebtLimit = collateralParamsDebtLimit.Add(cp.DebtLimit...)
if cp.DebtLimit.Denom != p.GlobalDebtLimit.Denom {
return fmt.Errorf("collateral debt limit denom %s does not match global debt limit denom %s",
cp.DebtLimit.Denom, p.GlobalDebtLimit.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)
collateralParamsDebtLimit = collateralParamsDebtLimit.Add(cp.DebtLimit.Amount)
if cp.DebtLimit.Amount.GT(p.GlobalDebtLimit.Amount) {
return fmt.Errorf("collateral debt limit %s exceeds global debt limit: %s", cp.DebtLimit, p.GlobalDebtLimit)
}
}
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 collateralParamsDebtLimit.GT(p.GlobalDebtLimit.Amount) {
return fmt.Errorf("sum of collateral debt limits %s exceeds global debt limit %s",
collateralParamsDebtLimit, p.GlobalDebtLimit)
}
return nil
}
func validateGlobalDebtLimitParam(i interface{}) error {
globalDebtLimit, ok := i.(sdk.Coins)
globalDebtLimit, ok := i.(sdk.Coin)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
@ -250,8 +271,12 @@ func validateCollateralParams(i interface{}) error {
collateralDupMap := make(map[string]bool)
prefixDupMap := make(map[int]bool)
for _, cp := range collateralParams {
if strings.TrimSpace(cp.Denom) == "" {
return fmt.Errorf("debt denom cannot be blank %s", cp)
if err := sdk.ValidateDenom(cp.Denom); err != nil {
return fmt.Errorf("collateral denom invalid %s", cp.Denom)
}
if strings.TrimSpace(cp.MarketID) == "" {
return fmt.Errorf("market id cannot be blank %s", cp)
}
prefix := int(cp.Prefix)
@ -283,39 +308,26 @@ func validateCollateralParams(i interface{}) error {
if !cp.AuctionSize.IsPositive() {
return fmt.Errorf("auction size should be positive, is %s for %s", cp.AuctionSize, cp.Denom)
}
if cp.StabilityFee.LT(sdk.OneDec()) {
return fmt.Errorf("stability fee must be ≥ 1.0, is %s for %s", cp.StabilityFee, cp.Denom)
if cp.StabilityFee.LT(sdk.OneDec()) || cp.StabilityFee.GT(stabilityFeeMax) {
return fmt.Errorf("stability fee must be ≥ 1.0, ≤ %s, is %s for %s", stabilityFeeMax, cp.StabilityFee, cp.Denom)
}
}
return nil
}
func validateDebtParams(i interface{}) error {
debtParams, ok := i.(DebtParams)
func validateDebtParam(i interface{}) error {
debtParam, ok := i.(DebtParam)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
// validate debt params
debtDenoms := make(map[string]bool)
for _, dp := range debtParams {
if strings.TrimSpace(dp.Denom) == "" {
return fmt.Errorf("debt denom cannot be blank %s", dp)
if err := sdk.ValidateDenom(debtParam.Denom); err != nil {
return fmt.Errorf("debt denom invalid %s", debtParam.Denom)
}
_, found := debtDenoms[dp.Denom]
if found {
return fmt.Errorf("duplicate debt denom: %s", dp.Denom)
if debtParam.SavingsRate.LT(sdk.ZeroDec()) || debtParam.SavingsRate.GT(sdk.OneDec()) {
return fmt.Errorf("savings rate should be between 0 and 1, is %s for %s", debtParam.SavingsRate, debtParam.Denom)
}
if dp.SavingsRate.LT(sdk.ZeroDec()) || dp.SavingsRate.GT(sdk.OneDec()) {
return fmt.Errorf("savings rate should be between 0 and 1, is %s for %s", dp.SavingsRate, dp.Denom)
}
debtDenoms[dp.Denom] = true
}
return nil
}

736
x/cdp/types/params_test.go Normal file
View File

@ -0,0 +1,736 @@
package types_test
import (
"strings"
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/cdp/types"
"github.com/stretchr/testify/suite"
)
type ParamsTestSuite struct {
suite.Suite
}
func (suite *ParamsTestSuite) SetupTest() {
}
func (suite *ParamsTestSuite) TestParamValidation() {
type args struct {
globalDebtLimit sdk.Coin
collateralParams types.CollateralParams
debtParam types.DebtParam
surplusThreshold sdk.Int
debtThreshold sdk.Int
distributionFreq time.Duration
breaker bool
}
type errArgs struct {
expectPass bool
contains string
}
testCases := []struct {
name string
args args
errArgs errArgs
}{
{
name: "default",
args: args{
globalDebtLimit: types.DefaultGlobalDebt,
collateralParams: types.DefaultCollateralParams,
debtParam: types.DefaultDebtParam,
surplusThreshold: types.DefaultSurplusThreshold,
debtThreshold: types.DefaultDebtThreshold,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: true,
contains: "",
},
},
{
name: "valid single-collateral",
args: args{
globalDebtLimit: sdk.NewInt64Coin("usdx", 4000000000000),
collateralParams: types.CollateralParams{
{
Denom: "bnb",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20,
MarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8),
},
},
debtParam: types.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
debtThreshold: types.DefaultDebtThreshold,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: true,
contains: "",
},
},
{
name: "invalid single-collateral mismatched debt denoms",
args: args{
globalDebtLimit: sdk.NewInt64Coin("usdx", 4000000000000),
collateralParams: types.CollateralParams{
{
Denom: "bnb",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20,
MarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8),
},
},
debtParam: types.DebtParam{
Denom: "susd",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
debtThreshold: types.DefaultDebtThreshold,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: false,
contains: "does not match global debt denom",
},
},
{
name: "invalid single-collateral over debt limit",
args: args{
globalDebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
collateralParams: types.CollateralParams{
{
Denom: "bnb",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20,
MarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8),
},
},
debtParam: types.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
debtThreshold: types.DefaultDebtThreshold,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: false,
contains: "exceeds global debt limit",
},
},
{
name: "valid multi-collateral",
args: args{
globalDebtLimit: sdk.NewInt64Coin("usdx", 4000000000000),
collateralParams: types.CollateralParams{
{
Denom: "bnb",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20,
MarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8),
},
{
Denom: "xrp",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x21,
MarketID: "xrp:usd",
ConversionFactor: sdk.NewInt(6),
},
},
debtParam: types.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
debtThreshold: types.DefaultDebtThreshold,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: true,
contains: "",
},
},
{
name: "invalid multi-collateral over debt limit",
args: args{
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{
{
Denom: "bnb",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20,
MarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8),
},
{
Denom: "xrp",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x21,
MarketID: "xrp:usd",
ConversionFactor: sdk.NewInt(6),
},
},
debtParam: types.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
debtThreshold: types.DefaultDebtThreshold,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: false,
contains: "sum of collateral debt limits",
},
},
{
name: "invalid multi-collateral multiple debt denoms",
args: args{
globalDebtLimit: sdk.NewInt64Coin("usdx", 4000000000000),
collateralParams: types.CollateralParams{
{
Denom: "bnb",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20,
MarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8),
},
{
Denom: "xrp",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("susd", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x21,
MarketID: "xrp:usd",
ConversionFactor: sdk.NewInt(6),
},
},
debtParam: types.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
debtThreshold: types.DefaultDebtThreshold,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: false,
contains: "does not match global debt limit denom",
},
},
{
name: "invalid collateral params empty denom",
args: args{
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{
{
Denom: "",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20,
MarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8),
},
},
debtParam: types.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
debtThreshold: types.DefaultDebtThreshold,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: false,
contains: "collateral denom invalid",
},
},
{
name: "invalid collateral params empty market id",
args: args{
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{
{
Denom: "bnb",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20,
MarketID: "",
ConversionFactor: sdk.NewInt(8),
},
},
debtParam: types.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
debtThreshold: types.DefaultDebtThreshold,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: false,
contains: "market id cannot be blank",
},
},
{
name: "invalid collateral params duplicate denom",
args: args{
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{
{
Denom: "bnb",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20,
MarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8),
},
{
Denom: "bnb",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x21,
MarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8),
},
},
debtParam: types.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
debtThreshold: types.DefaultDebtThreshold,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: false,
contains: "duplicate collateral denom",
},
},
{
name: "invalid collateral params duplicate prefix",
args: args{
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{
{
Denom: "bnb",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20,
MarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8),
},
{
Denom: "xrp",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20,
MarketID: "xrp:usd",
ConversionFactor: sdk.NewInt(8),
},
},
debtParam: types.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
debtThreshold: types.DefaultDebtThreshold,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: false,
contains: "duplicate prefix for collateral denom",
},
},
{
name: "invalid collateral params nil debt limit",
args: args{
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{
{
Denom: "bnb",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.Coin{},
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20,
MarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8),
},
},
debtParam: types.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
debtThreshold: types.DefaultDebtThreshold,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: false,
contains: "debt limit for all collaterals should be positive",
},
},
{
name: "invalid collateral params liquidation ratio out of range",
args: args{
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{
{
Denom: "bnb",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("1.05"),
AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20,
MarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8),
},
},
debtParam: types.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
debtThreshold: types.DefaultDebtThreshold,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: false,
contains: "liquidation penalty should be between 0 and 1",
},
},
{
name: "invalid collateral params auction size zero",
args: args{
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{
{
Denom: "bnb",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.ZeroInt(),
Prefix: 0x20,
MarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8),
},
},
debtParam: types.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
debtThreshold: types.DefaultDebtThreshold,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: false,
contains: "auction size should be positive",
},
},
{
name: "invalid collateral params stability fee out of range",
args: args{
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{
{
Denom: "bnb",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.1"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20,
MarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8),
},
},
debtParam: types.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
debtThreshold: types.DefaultDebtThreshold,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: false,
contains: "stability fee must be ≥ 1.0",
},
},
{
name: "invalid debt param empty denom",
args: args{
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{
{
Denom: "bnb",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20,
MarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8),
},
},
debtParam: types.DebtParam{
Denom: "",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
debtThreshold: types.DefaultDebtThreshold,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: false,
contains: "debt denom invalid",
},
},
{
name: "invalid debt param savings rate out of range",
args: args{
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{
{
Denom: "bnb",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20,
MarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8),
},
},
debtParam: types.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("1.05"),
},
surplusThreshold: types.DefaultSurplusThreshold,
debtThreshold: types.DefaultDebtThreshold,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: false,
contains: "savings rate should be between 0 and 1",
},
},
{
name: "nil debt limit",
args: args{
globalDebtLimit: sdk.Coin{},
collateralParams: types.DefaultCollateralParams,
debtParam: types.DefaultDebtParam,
surplusThreshold: types.DefaultSurplusThreshold,
debtThreshold: types.DefaultDebtThreshold,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: false,
contains: "invalid coins: global debt limit",
},
},
{
name: "zero savings distribution frequency",
args: args{
globalDebtLimit: types.DefaultGlobalDebt,
collateralParams: types.DefaultCollateralParams,
debtParam: types.DefaultDebtParam,
surplusThreshold: types.DefaultSurplusThreshold,
debtThreshold: types.DefaultDebtThreshold,
distributionFreq: time.Second * 0,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: false,
contains: "savings distribution frequency should be positive",
},
},
{
name: "zero surplus auction",
args: args{
globalDebtLimit: types.DefaultGlobalDebt,
collateralParams: types.DefaultCollateralParams,
debtParam: types.DefaultDebtParam,
surplusThreshold: sdk.ZeroInt(),
debtThreshold: types.DefaultDebtThreshold,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: false,
contains: "surplus auction threshold should be positive",
},
},
{
name: "zero debt auction",
args: args{
globalDebtLimit: types.DefaultGlobalDebt,
collateralParams: types.DefaultCollateralParams,
debtParam: types.DefaultDebtParam,
surplusThreshold: types.DefaultSurplusThreshold,
debtThreshold: sdk.ZeroInt(),
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: false,
contains: "debt auction threshold should be positive",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
params := types.NewParams(tc.args.globalDebtLimit, tc.args.collateralParams, tc.args.debtParam, tc.args.surplusThreshold, tc.args.debtThreshold, tc.args.distributionFreq, tc.args.breaker)
err := params.Validate()
if tc.errArgs.expectPass {
suite.Require().NoError(err)
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}
func TestParamsTestSuite(t *testing.T) {
suite.Run(t, new(ParamsTestSuite))
}

13
x/incentive/abci.go Normal file
View File

@ -0,0 +1,13 @@
package incentive
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/incentive/keeper"
)
// BeginBlocker runs at the start of every block
func BeginBlocker(ctx sdk.Context, k keeper.Keeper) {
k.DeleteExpiredClaimsAndClaimPeriods(ctx)
k.ApplyRewardsToCdps(ctx)
k.CreateAndDeleteRewardPeriods(ctx)
}

92
x/incentive/alias.go Normal file
View File

@ -0,0 +1,92 @@
// nolint
// autogenerated code using github.com/rigelrozanski/multitool
// aliases generated for the following subdirectories:
// ALIASGEN: github.com/kava-labs/kava/x/incentive/keeper
// ALIASGEN: github.com/kava-labs/kava/x/incentive/types
package incentive
import (
"github.com/kava-labs/kava/x/incentive/keeper"
"github.com/kava-labs/kava/x/incentive/types"
)
const (
EventTypeClaim = types.EventTypeClaim
AttributeValueCategory = types.AttributeValueCategory
AttributeKeySender = types.AttributeKeySender
ModuleName = types.ModuleName
StoreKey = types.StoreKey
RouterKey = types.RouterKey
DefaultParamspace = types.DefaultParamspace
QuerierRoute = types.QuerierRoute
QueryGetClaims = types.QueryGetClaims
RestClaimOwner = types.RestClaimOwner
RestClaimDenom = types.RestClaimDenom
QueryGetParams = types.QueryGetParams
)
var (
// functions aliases
NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier
GetTotalVestingPeriodLength = types.GetTotalVestingPeriodLength
RegisterCodec = types.RegisterCodec
NewGenesisState = types.NewGenesisState
DefaultGenesisState = types.DefaultGenesisState
BytesToUint64 = types.BytesToUint64
GetClaimPeriodPrefix = types.GetClaimPeriodPrefix
GetClaimPrefix = types.GetClaimPrefix
NewMsgClaimReward = types.NewMsgClaimReward
NewParams = types.NewParams
DefaultParams = types.DefaultParams
ParamKeyTable = types.ParamKeyTable
NewReward = types.NewReward
NewPeriod = types.NewPeriod
NewQueryClaimsParams = types.NewQueryClaimsParams
NewRewardPeriod = types.NewRewardPeriod
NewClaimPeriod = types.NewClaimPeriod
NewClaim = types.NewClaim
// variable aliases
ModuleCdc = types.ModuleCdc
ErrClaimNotFound = types.ErrClaimNotFound
ErrClaimPeriodNotFound = types.ErrClaimPeriodNotFound
ErrInvalidAccountType = types.ErrInvalidAccountType
ErrNoClaimsFound = types.ErrNoClaimsFound
ErrInsufficientModAccountBalance = types.ErrInsufficientModAccountBalance
RewardPeriodKeyPrefix = types.RewardPeriodKeyPrefix
ClaimPeriodKeyPrefix = types.ClaimPeriodKeyPrefix
ClaimKeyPrefix = types.ClaimKeyPrefix
NextClaimPeriodIDPrefix = types.NextClaimPeriodIDPrefix
PreviousBlockTimeKey = types.PreviousBlockTimeKey
KeyActive = types.KeyActive
KeyRewards = types.KeyRewards
DefaultActive = types.DefaultActive
DefaultRewards = types.DefaultRewards
DefaultPreviousBlockTime = types.DefaultPreviousBlockTime
GovDenom = types.GovDenom
PrincipalDenom = types.PrincipalDenom
IncentiveMacc = types.IncentiveMacc
)
type (
Keeper = keeper.Keeper
SupplyKeeper = types.SupplyKeeper
CdpKeeper = types.CdpKeeper
AccountKeeper = types.AccountKeeper
GenesisClaimPeriodID = types.GenesisClaimPeriodID
GenesisClaimPeriodIDs = types.GenesisClaimPeriodIDs
GenesisState = types.GenesisState
MsgClaimReward = types.MsgClaimReward
Params = types.Params
Reward = types.Reward
Rewards = types.Rewards
QueryClaimsParams = types.QueryClaimsParams
PostClaimReq = types.PostClaimReq
RewardPeriod = types.RewardPeriod
RewardPeriods = types.RewardPeriods
ClaimPeriod = types.ClaimPeriod
ClaimPeriods = types.ClaimPeriods
Claim = types.Claim
Claims = types.Claims
)

View File

@ -0,0 +1,98 @@
package cli
import (
"fmt"
"strings"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version"
"github.com/kava-labs/kava/x/incentive/types"
"github.com/spf13/cobra"
)
// GetQueryCmd returns the cli query commands for the incentive module
func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
incentiveQueryCmd := &cobra.Command{
Use: types.ModuleName,
Short: "Querying commands for the incentive module",
}
incentiveQueryCmd.AddCommand(flags.GetCommands(
queryParamsCmd(queryRoute, cdc),
queryClaimsCmd(queryRoute, cdc),
)...)
return incentiveQueryCmd
}
func queryClaimsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "claims [owner-addr] [denom]",
Short: "get claims by onwer and denom",
Long: strings.TrimSpace(
fmt.Sprintf(`Get all claims owned by the owner address for the particular collateral type.
Example:
$ %s query %s claims kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw bnb`, version.ClientName, types.ModuleName)),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Prepare params for querier
ownerAddress, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
bz, err := cdc.MarshalJSON(types.QueryClaimsParams{
Owner: ownerAddress,
Denom: args[1],
})
if err != nil {
return err
}
// Query
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetClaims)
res, _, err := cliCtx.QueryWithData(route, bz)
if err != nil {
return err
}
var claims types.Claims
if err := cdc.UnmarshalJSON(res, &claims); err != nil {
return fmt.Errorf("failed to unmarshal claims: %w", err)
}
return cliCtx.PrintOutput(claims)
},
}
}
func queryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "params",
Short: "get the incentive module parameters",
Long: "Get the current global incentive module parameters.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Query
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetParams)
res, _, err := cliCtx.QueryWithData(route, nil)
if err != nil {
return err
}
// Decode and print results
var params types.Params
if err := cdc.UnmarshalJSON(res, &params); err != nil {
return fmt.Errorf("failed to unmarshal params: %w", err)
}
return cliCtx.PrintOutput(params)
},
}
}

View File

@ -0,0 +1,63 @@
package cli
import (
"bufio"
"fmt"
"strings"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/kava-labs/kava/x/incentive/types"
"github.com/spf13/cobra"
)
// GetTxCmd returns the transaction cli commands for the incentive module
func GetTxCmd(cdc *codec.Codec) *cobra.Command {
incentiveTxCmd := &cobra.Command{
Use: types.ModuleName,
Short: "transaction commands for the incentive module",
}
incentiveTxCmd.AddCommand(flags.PostCommands(
getCmdClaim(cdc),
)...)
return incentiveTxCmd
}
func getCmdClaim(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "claim [owner] [denom]",
Short: "claim rewards for owner and denom",
Long: strings.TrimSpace(
fmt.Sprintf(`Claim any outstanding rewards owned by owner for the input denom,
Example:
$ %s tx %s claim kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw bnb
`, version.ClientName, types.ModuleName),
),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
inBuf := bufio.NewReader(cmd.InOrStdin())
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
owner, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
msg := types.NewMsgClaimReward(owner, args[1])
err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}

View File

@ -0,0 +1,70 @@
package rest
import (
"fmt"
"net/http"
"github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/gorilla/mux"
"github.com/kava-labs/kava/x/incentive/types"
)
func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc(fmt.Sprintf("/%s/claims", types.ModuleName), queryClaimsHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/%s/parameters", types.ModuleName), queryParamsHandlerFn(cliCtx)).Methods("GET")
}
func queryClaimsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
vars := mux.Vars(r)
ownerBech32 := vars[types.RestClaimOwner]
denom := vars[types.RestClaimDenom]
owner, err := sdk.AccAddressFromBech32(ownerBech32)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
queryParams := types.NewQueryClaimsParams(owner, denom)
bz, err := cliCtx.Codec.MarshalJSON(queryParams)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/incentive/%s", types.QueryGetClaims), bz)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}
func queryParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
route := fmt.Sprintf("custom/%s/parameters", types.QuerierRoute)
res, height, err := cliCtx.QueryWithData(route, nil)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}

View File

@ -0,0 +1,13 @@
package rest
import (
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
)
// RegisterRoutes registers incentive-related REST handlers to a router
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
registerQueryRoutes(cliCtx, r)
registerTxRoutes(cliCtx, r)
}

View File

@ -0,0 +1,36 @@
package rest
import (
"net/http"
"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/gorilla/mux"
"github.com/kava-labs/kava/x/incentive/types"
)
func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc("/incentive/claim", postClaimHandlerFn(cliCtx)).Methods("POST")
}
func postClaimHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var requestBody types.PostClaimReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) {
return
}
requestBody.BaseReq = requestBody.BaseReq.Sanitize()
if !requestBody.BaseReq.ValidateBasic(w) {
return
}
msg := types.NewMsgClaimReward(requestBody.Sender, requestBody.Denom)
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg})
}
}

40
x/incentive/doc.go Normal file
View File

@ -0,0 +1,40 @@
/*
Package incentive implements a Cosmos SDK module, per ADR 009, that provides governance-controlled,
on-chain incentives for users who open cdps and mint stablecoins (USDX).
For the background and motivation of this module, see the governance proposal that was voted on by
KAVA token holders: https://ipfs.io/ipfs/QmSYedssC3nyQacDJmNcREtgmTPyaMx2JX7RNkMdAVkdkr/user-growth-fund-proposal.pdf
The 'Reward' parameter is used to control how much incentives are given.
For example, the following reward:
Reward{
Active: true,
Denom: "bnb",
AvailableRewards: sdk.NewCoin("ukava", 1000000000),
Duration: time.Hour*7*24,
TimeLock: time.Hour*24*365,
ClaimDuration: time.Hour*7*24,
}
will distribute 1000 KAVA each week (Duration) to users who mint USDX using collateral bnb.
That KAVA can be claimed by the user for one week (ClaimDuration) after the reward period expires,
and all KAVA rewards will be timelocked for 1 year (TimeLock). If a user does not claim them during
the claim duration period, they are forgone.
Rewards are accumulated by users continuously and proportionally - ie. if a user holds a CDP that has
minted 10% of all USDX backed by bnb for the entire reward period, they will be eligible to claim 10%
of rewards for that period.
Once a reward period ends, but not before, users can claim the rewards they have accumulated. Users claim rewards
using a MsgClaimReward transaction. The following msg:
MsgClaimReward {
"kava1..."
"bnb"
}
will claim all outstanding rewards for minting USDX backed by bnb for the input user.
*/
package incentive

73
x/incentive/genesis.go Normal file
View File

@ -0,0 +1,73 @@
package incentive
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/incentive/keeper"
"github.com/kava-labs/kava/x/incentive/types"
)
// InitGenesis initializes the store state from a genesis state.
func InitGenesis(ctx sdk.Context, k keeper.Keeper, supplyKeeper types.SupplyKeeper, gs types.GenesisState) {
// check if the module account exists
moduleAcc := supplyKeeper.GetModuleAccount(ctx, types.IncentiveMacc)
if moduleAcc == nil {
panic(fmt.Sprintf("%s module account has not been set", types.IncentiveMacc))
}
if err := gs.Validate(); err != nil {
panic(fmt.Sprintf("failed to validate %s genesis state: %s", types.ModuleName, err))
}
k.SetParams(ctx, gs.Params)
for _, r := range gs.Params.Rewards {
k.SetNextClaimPeriodID(ctx, r.Denom, 1)
}
// only set the previous block time if it's different than default
if !gs.PreviousBlockTime.Equal(types.DefaultPreviousBlockTime) {
k.SetPreviousBlockTime(ctx, gs.PreviousBlockTime)
}
// set store objects
for _, rp := range gs.RewardPeriods {
k.SetRewardPeriod(ctx, rp)
}
for _, cp := range gs.ClaimPeriods {
k.SetClaimPeriod(ctx, cp)
}
for _, c := range gs.Claims {
k.SetClaim(ctx, c)
}
for _, id := range gs.NextClaimPeriodIDs {
k.SetNextClaimPeriodID(ctx, id.Denom, id.ID)
}
}
// ExportGenesis export genesis state for incentive module
func ExportGenesis(ctx sdk.Context, k keeper.Keeper) types.GenesisState {
// get all objects out of the store
params := k.GetParams(ctx)
previousBlockTime, found := k.GetPreviousBlockTime(ctx)
// since it is not set in genesis, if somehow the chain got started and was exported
// immediately after InitGenesis, there would be no previousBlockTime value.
if !found {
previousBlockTime = types.DefaultPreviousBlockTime
}
// Get all objects from the store
rewardPeriods := k.GetAllRewardPeriods(ctx)
claimPeriods := k.GetAllClaimPeriods(ctx)
claims := k.GetAllClaims(ctx)
claimPeriodIDs := k.GetAllClaimPeriodIDPairs(ctx)
return types.NewGenesisState(params, previousBlockTime, rewardPeriods, claimPeriods, claims, claimPeriodIDs)
}

48
x/incentive/handler.go Normal file
View File

@ -0,0 +1,48 @@
package incentive
import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/incentive/keeper"
"github.com/kava-labs/kava/x/incentive/types"
)
// NewHandler creates an sdk.Handler for incentive module messages
func NewHandler(k keeper.Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
ctx = ctx.WithEventManager(sdk.NewEventManager())
switch msg := msg.(type) {
case types.MsgClaimReward:
return handleMsgClaimReward(ctx, k, msg)
default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg)
}
}
}
func handleMsgClaimReward(ctx sdk.Context, k keeper.Keeper, msg types.MsgClaimReward) (*sdk.Result, error) {
claims, found := k.GetClaimsByAddressAndDenom(ctx, msg.Sender, msg.Denom)
if !found {
return nil, sdkerrors.Wrapf(types.ErrNoClaimsFound, "address: %s, denom: %s", msg.Sender, msg.Denom)
}
for _, claim := range claims {
err := k.PayoutClaim(ctx, claim.Owner, claim.Denom, claim.ClaimPeriodID)
if err != nil {
return nil, err
}
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()),
),
)
return &sdk.Result{
Events: ctx.EventManager().Events(),
}, nil
}

View File

@ -0,0 +1,74 @@
package incentive_test
import (
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/incentive"
"github.com/kava-labs/kava/x/kavadist"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
)
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
type HandlerTestSuite struct {
suite.Suite
ctx sdk.Context
app app.TestApp
handler sdk.Handler
keeper incentive.Keeper
addrs []sdk.AccAddress
}
func (suite *HandlerTestSuite) SetupTest() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
keeper := tApp.GetIncentiveKeeper()
// Set up genesis state and initialize
_, addrs := app.GeneratePrivKeyAddressPairs(3)
coins := []sdk.Coins{}
for j := 0; j < 3; j++ {
coins = append(coins, cs(c("bnb", 10000000000), c("ukava", 10000000000)))
}
authGS := app.NewAuthGenState(addrs, coins)
tApp.InitializeFromGenesisStates(authGS)
suite.addrs = addrs
suite.handler = incentive.NewHandler(keeper)
suite.keeper = keeper
suite.app = tApp
suite.ctx = ctx
}
func (suite *HandlerTestSuite) addClaim() {
supplyKeeper := suite.app.GetSupplyKeeper()
macc := supplyKeeper.GetModuleAccount(suite.ctx, kavadist.ModuleName)
err := supplyKeeper.MintCoins(suite.ctx, macc.GetName(), cs(c("ukava", 1000000)))
suite.Require().NoError(err)
cp := incentive.NewClaimPeriod("bnb", 1, suite.ctx.BlockTime().Add(time.Hour*168), time.Hour*8766)
suite.NotPanics(func() {
suite.keeper.SetClaimPeriod(suite.ctx, cp)
})
c1 := incentive.NewClaim(suite.addrs[0], c("ukava", 1000000), "bnb", 1)
suite.NotPanics(func() {
suite.keeper.SetClaim(suite.ctx, c1)
})
}
func (suite *HandlerTestSuite) TestMsgClaimReward() {
suite.addClaim()
msg := incentive.NewMsgClaimReward(suite.addrs[0], "bnb")
res, err := suite.handler(suite.ctx, msg)
suite.NoError(err)
suite.Require().NotNil(res)
}
func TestHandlerTestSuite(t *testing.T) {
suite.Run(t, new(HandlerTestSuite))
}

View File

@ -0,0 +1,243 @@
package keeper
import (
"time"
"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/incentive/types"
)
// Keeper keeper for the incentive module
type Keeper struct {
accountKeeper types.AccountKeeper
cdc *codec.Codec
cdpKeeper types.CdpKeeper
key sdk.StoreKey
paramSubspace subspace.Subspace
supplyKeeper types.SupplyKeeper
}
// NewKeeper creates a new keeper
func NewKeeper(
cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, sk types.SupplyKeeper,
cdpk types.CdpKeeper, ak types.AccountKeeper,
) Keeper {
return Keeper{
accountKeeper: ak,
cdc: cdc,
cdpKeeper: cdpk,
key: key,
paramSubspace: paramstore.WithKeyTable(types.ParamKeyTable()),
supplyKeeper: sk,
}
}
// GetRewardPeriod returns the reward period from the store for the input denom and a boolean for if it was found
func (k Keeper) GetRewardPeriod(ctx sdk.Context, denom string) (types.RewardPeriod, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.RewardPeriodKeyPrefix)
bz := store.Get([]byte(denom))
if bz == nil {
return types.RewardPeriod{}, false
}
var rp types.RewardPeriod
k.cdc.MustUnmarshalBinaryBare(bz, &rp)
return rp, true
}
// SetRewardPeriod sets the reward period in the store for the input deno,
func (k Keeper) SetRewardPeriod(ctx sdk.Context, rp types.RewardPeriod) {
store := prefix.NewStore(ctx.KVStore(k.key), types.RewardPeriodKeyPrefix)
bz := k.cdc.MustMarshalBinaryBare(rp)
store.Set([]byte(rp.Denom), bz)
}
// DeleteRewardPeriod deletes the reward period in the store for the input denom,
func (k Keeper) DeleteRewardPeriod(ctx sdk.Context, denom string) {
store := prefix.NewStore(ctx.KVStore(k.key), types.RewardPeriodKeyPrefix)
store.Delete([]byte(denom))
}
// IterateRewardPeriods iterates over all reward period objects in the store and preforms a callback function
func (k Keeper) IterateRewardPeriods(ctx sdk.Context, cb func(rp types.RewardPeriod) (stop bool)) {
store := prefix.NewStore(ctx.KVStore(k.key), types.RewardPeriodKeyPrefix)
iterator := sdk.KVStorePrefixIterator(store, []byte{})
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var rp types.RewardPeriod
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &rp)
if cb(rp) {
break
}
}
}
// GetAllRewardPeriods returns all reward periods in the store
func (k Keeper) GetAllRewardPeriods(ctx sdk.Context) types.RewardPeriods {
rps := types.RewardPeriods{}
k.IterateRewardPeriods(ctx, func(rp types.RewardPeriod) (stop bool) {
rps = append(rps, rp)
return false
})
return rps
}
// GetNextClaimPeriodID returns the highest claim period id in the store for the input denom
func (k Keeper) GetNextClaimPeriodID(ctx sdk.Context, denom string) uint64 {
store := prefix.NewStore(ctx.KVStore(k.key), types.NextClaimPeriodIDPrefix)
bz := store.Get([]byte(denom))
return types.BytesToUint64(bz)
}
// SetNextClaimPeriodID sets the highest claim period id in the store for the input denom
func (k Keeper) SetNextClaimPeriodID(ctx sdk.Context, denom string, id uint64) {
store := prefix.NewStore(ctx.KVStore(k.key), types.NextClaimPeriodIDPrefix)
store.Set([]byte(denom), sdk.Uint64ToBigEndian(id))
}
// IterateClaimPeriodIDKeysAndValues iterates over the claim period id (value) and denom (key) of each claim period id in the store and performs a callback function
func (k Keeper) IterateClaimPeriodIDKeysAndValues(ctx sdk.Context, cb func(denom string, id uint64) (stop bool)) {
store := prefix.NewStore(ctx.KVStore(k.key), types.NextClaimPeriodIDPrefix)
iterator := sdk.KVStorePrefixIterator(store, []byte{})
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
id := types.BytesToUint64(iterator.Value())
denom := string(iterator.Key())
if cb(denom, id) {
break
}
}
}
// GetAllClaimPeriodIDPairs returns all denom:nextClaimPeriodID pairs in the store
func (k Keeper) GetAllClaimPeriodIDPairs(ctx sdk.Context) types.GenesisClaimPeriodIDs {
ids := types.GenesisClaimPeriodIDs{}
k.IterateClaimPeriodIDKeysAndValues(ctx, func(denom string, id uint64) (stop bool) {
genID := types.GenesisClaimPeriodID{
Denom: denom,
ID: id,
}
ids = append(ids, genID)
return false
})
return ids
}
// GetClaimPeriod returns claim period in the store for the input ID and denom and a boolean for if it was found
func (k Keeper) GetClaimPeriod(ctx sdk.Context, id uint64, denom string) (types.ClaimPeriod, bool) {
var cp types.ClaimPeriod
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimPeriodKeyPrefix)
bz := store.Get(types.GetClaimPeriodPrefix(denom, id))
if bz == nil {
return types.ClaimPeriod{}, false
}
k.cdc.MustUnmarshalBinaryBare(bz, &cp)
return cp, true
}
// SetClaimPeriod sets the claim period in the store for the input ID and denom
func (k Keeper) SetClaimPeriod(ctx sdk.Context, cp types.ClaimPeriod) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimPeriodKeyPrefix)
bz := k.cdc.MustMarshalBinaryBare(cp)
store.Set(types.GetClaimPeriodPrefix(cp.Denom, cp.ID), bz)
}
// DeleteClaimPeriod deletes the claim period in the store for the input ID and denom
func (k Keeper) DeleteClaimPeriod(ctx sdk.Context, id uint64, denom string) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimPeriodKeyPrefix)
store.Delete(types.GetClaimPeriodPrefix(denom, id))
}
// IterateClaimPeriods iterates over all claim period objects in the store and preforms a callback function
func (k Keeper) IterateClaimPeriods(ctx sdk.Context, cb func(cp types.ClaimPeriod) (stop bool)) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimPeriodKeyPrefix)
iterator := sdk.KVStorePrefixIterator(store, []byte{})
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var cp types.ClaimPeriod
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &cp)
if cb(cp) {
break
}
}
}
// GetAllClaimPeriods returns all ClaimPeriod objects in the store
func (k Keeper) GetAllClaimPeriods(ctx sdk.Context) types.ClaimPeriods {
cps := types.ClaimPeriods{}
k.IterateClaimPeriods(ctx, func(cp types.ClaimPeriod) (stop bool) {
cps = append(cps, cp)
return false
})
return cps
}
// GetClaim returns the claim in the store corresponding the the input address denom and id and a boolean for if the claim was found
func (k Keeper) GetClaim(ctx sdk.Context, addr sdk.AccAddress, denom string, id uint64) (types.Claim, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimKeyPrefix)
bz := store.Get(types.GetClaimPrefix(addr, denom, id))
if bz == nil {
return types.Claim{}, false
}
var c types.Claim
k.cdc.MustUnmarshalBinaryBare(bz, &c)
return c, true
}
// SetClaim sets the claim in the store corresponding to the input address, denom, and id
func (k Keeper) SetClaim(ctx sdk.Context, c types.Claim) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimKeyPrefix)
bz := k.cdc.MustMarshalBinaryBare(c)
store.Set(types.GetClaimPrefix(c.Owner, c.Denom, c.ClaimPeriodID), bz)
}
// DeleteClaim deletes the claim in the store corresponding to the input address, denom, and id
func (k Keeper) DeleteClaim(ctx sdk.Context, owner sdk.AccAddress, denom string, id uint64) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimKeyPrefix)
store.Delete(types.GetClaimPrefix(owner, denom, id))
}
// IterateClaims iterates over all claim objects in the store and preforms a callback function
func (k Keeper) IterateClaims(ctx sdk.Context, cb func(c types.Claim) (stop bool)) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimKeyPrefix)
iterator := sdk.KVStorePrefixIterator(store, []byte{})
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var c types.Claim
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &c)
if cb(c) {
break
}
}
}
// GetAllClaims returns all Claim objects in the store
func (k Keeper) GetAllClaims(ctx sdk.Context) types.Claims {
cs := types.Claims{}
k.IterateClaims(ctx, func(c types.Claim) (stop bool) {
cs = append(cs, c)
return false
})
return cs
}
// 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.MustUnmarshalBinaryBare(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.MustMarshalBinaryBare(blockTime))
}

View File

@ -0,0 +1,174 @@
package keeper_test
import (
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/incentive/keeper"
"github.com/kava-labs/kava/x/incentive/types"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
)
// Test suite used for all keeper tests
type KeeperTestSuite struct {
suite.Suite
keeper keeper.Keeper
app app.TestApp
ctx sdk.Context
addrs []sdk.AccAddress
}
// The default state used by each test
func (suite *KeeperTestSuite) SetupTest() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
tApp.InitializeFromGenesisStates()
_, addrs := app.GeneratePrivKeyAddressPairs(1)
keeper := tApp.GetIncentiveKeeper()
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
suite.addrs = addrs
}
func (suite *KeeperTestSuite) getAccount(addr sdk.AccAddress) authexported.Account {
ak := suite.app.GetAccountKeeper()
return ak.GetAccount(suite.ctx, addr)
}
func (suite *KeeperTestSuite) getModuleAccount(name string) supplyexported.ModuleAccountI {
sk := suite.app.GetSupplyKeeper()
return sk.GetModuleAccount(suite.ctx, name)
}
func (suite *KeeperTestSuite) TestGetSetDeleteRewardPeriod() {
rp := types.NewRewardPeriod("bnb", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), time.Hour*8766)
_, found := suite.keeper.GetRewardPeriod(suite.ctx, "bnb")
suite.False(found)
suite.NotPanics(func() {
suite.keeper.SetRewardPeriod(suite.ctx, rp)
})
testRP, found := suite.keeper.GetRewardPeriod(suite.ctx, "bnb")
suite.True(found)
suite.Equal(rp, testRP)
suite.NotPanics(func() {
suite.keeper.DeleteRewardPeriod(suite.ctx, "bnb")
})
_, found = suite.keeper.GetRewardPeriod(suite.ctx, "bnb")
suite.False(found)
}
func (suite *KeeperTestSuite) TestGetSetDeleteClaimPeriod() {
cp := types.NewClaimPeriod("bnb", 1, suite.ctx.BlockTime().Add(time.Hour*168), time.Hour*8766)
_, found := suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
suite.False(found)
suite.NotPanics(func() {
suite.keeper.SetClaimPeriod(suite.ctx, cp)
})
testCP, found := suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
suite.True(found)
suite.Equal(cp, testCP)
suite.NotPanics(func() {
suite.keeper.DeleteClaimPeriod(suite.ctx, 1, "bnb")
})
_, found = suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
suite.False(found)
}
func (suite *KeeperTestSuite) TestGetSetClaimPeriodID() {
suite.Panics(func() {
suite.keeper.GetNextClaimPeriodID(suite.ctx, "bnb")
})
suite.NotPanics(func() {
suite.keeper.SetNextClaimPeriodID(suite.ctx, "bnb", 1)
})
testID := suite.keeper.GetNextClaimPeriodID(suite.ctx, "bnb")
suite.Equal(uint64(1), testID)
}
func (suite *KeeperTestSuite) TestGetSetDeleteClaim() {
c := types.NewClaim(suite.addrs[0], c("ukava", 1000000), "bnb", 1)
_, found := suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "bnb", 1)
suite.False(found)
suite.NotPanics(func() {
suite.keeper.SetClaim(suite.ctx, c)
})
testC, found := suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "bnb", 1)
suite.True(found)
suite.Equal(c, testC)
suite.NotPanics(func() {
suite.keeper.DeleteClaim(suite.ctx, suite.addrs[0], "bnb", 1)
})
_, found = suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "bnb", 1)
suite.False(found)
}
func (suite *KeeperTestSuite) TestIterateMethods() {
suite.addObjectsToStore() // adds 2 objects of each type to the store
var rewardPeriods types.RewardPeriods
suite.keeper.IterateRewardPeriods(suite.ctx, func(rp types.RewardPeriod) (stop bool) {
rewardPeriods = append(rewardPeriods, rp)
return false
})
suite.Equal(2, len(rewardPeriods))
var claimPeriods types.ClaimPeriods
suite.keeper.IterateClaimPeriods(suite.ctx, func(cp types.ClaimPeriod) (stop bool) {
claimPeriods = append(claimPeriods, cp)
return false
})
suite.Equal(2, len(claimPeriods))
var claims types.Claims
suite.keeper.IterateClaims(suite.ctx, func(c types.Claim) (stop bool) {
claims = append(claims, c)
return false
})
suite.Equal(2, len(claims))
var genIDs types.GenesisClaimPeriodIDs
suite.keeper.IterateClaimPeriodIDKeysAndValues(suite.ctx, func(denom string, id uint64) (stop bool) {
genID := types.GenesisClaimPeriodID{Denom: denom, ID: id}
genIDs = append(genIDs, genID)
return false
})
suite.Equal(2, len(genIDs))
}
func (suite *KeeperTestSuite) addObjectsToStore() {
rp1 := types.NewRewardPeriod("bnb", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), time.Hour*8766)
rp2 := types.NewRewardPeriod("xrp", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), time.Hour*8766)
suite.keeper.SetRewardPeriod(suite.ctx, rp1)
suite.keeper.SetRewardPeriod(suite.ctx, rp2)
cp1 := types.NewClaimPeriod("bnb", 1, suite.ctx.BlockTime().Add(time.Hour*168), time.Hour*8766)
cp2 := types.NewClaimPeriod("xrp", 1, suite.ctx.BlockTime().Add(time.Hour*168), time.Hour*8766)
suite.keeper.SetClaimPeriod(suite.ctx, cp1)
suite.keeper.SetClaimPeriod(suite.ctx, cp2)
suite.keeper.SetNextClaimPeriodID(suite.ctx, "bnb", 1)
suite.keeper.SetNextClaimPeriodID(suite.ctx, "xrp", 1)
c1 := types.NewClaim(suite.addrs[0], c("ukava", 1000000), "bnb", 1)
c2 := types.NewClaim(suite.addrs[0], c("ukava", 1000000), "xrp", 1)
suite.keeper.SetClaim(suite.ctx, c1)
suite.keeper.SetClaim(suite.ctx, c2)
params := types.NewParams(
true, types.Rewards{types.NewReward(true, "bnb", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24)},
)
suite.keeper.SetParams(suite.ctx, params)
}
func TestKeeperTestSuite(t *testing.T) {
suite.Run(t, new(KeeperTestSuite))
}

View File

@ -0,0 +1,18 @@
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/incentive/types"
)
// GetParams returns the params from the store
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, params types.Params) {
k.paramSubspace.SetParamSet(ctx, &params)
}

View File

@ -0,0 +1,204 @@
package keeper
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/auth"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
supplyExported "github.com/cosmos/cosmos-sdk/x/supply/exported"
"github.com/kava-labs/kava/x/incentive/types"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
)
// PayoutClaim sends the timelocked claim coins to the input address
func (k Keeper) PayoutClaim(ctx sdk.Context, addr sdk.AccAddress, denom string, id uint64) error {
claim, found := k.GetClaim(ctx, addr, denom, id)
if !found {
return sdkerrors.Wrapf(types.ErrClaimNotFound, "id: %d, denom %s, address: %s", id, denom, addr)
}
claimPeriod, found := k.GetClaimPeriod(ctx, id, denom)
if !found {
return sdkerrors.Wrapf(types.ErrClaimPeriodNotFound, "id: %d, denom: %s", id, denom)
}
err := k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, addr, sdk.NewCoins(claim.Reward), int64(claimPeriod.TimeLock.Seconds()))
if err != nil {
return err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeClaim,
sdk.NewAttribute(types.AttributeKeySender, fmt.Sprintf("%s", addr)),
),
)
return nil
}
// SendTimeLockedCoinsToAccount sends time-locked coins from the input module account to the recipient. If the recipients account is not a vesting account, it is converted to a periodic vesting account and the coins are added to the vesting balance as a vesting period with the input length.
func (k Keeper) SendTimeLockedCoinsToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins, length int64) error {
macc := k.supplyKeeper.GetModuleAccount(ctx, senderModule)
if !macc.GetCoins().IsAllGTE(amt) {
return sdkerrors.Wrapf(types.ErrInsufficientModAccountBalance, "%s", senderModule)
}
// 0. Get the account from the account keeper and do a type switch, error if it's a validator vesting account or module account (can make this work for validator vesting later if necessary)
acc := k.accountKeeper.GetAccount(ctx, recipientAddr)
switch acc.(type) {
case *validatorvesting.ValidatorVestingAccount, supplyExported.ModuleAccountI:
return sdkerrors.Wrapf(types.ErrInvalidAccountType, "%T", acc)
case *vesting.PeriodicVestingAccount:
return k.SendTimeLockedCoinsToPeriodicVestingAccount(ctx, senderModule, recipientAddr, amt, length)
case *auth.BaseAccount:
return k.SendTimeLockedCoinsToBaseAccount(ctx, senderModule, recipientAddr, amt, length)
default:
return sdkerrors.Wrapf(types.ErrInvalidAccountType, "%T", acc)
}
}
// SendTimeLockedCoinsToPeriodicVestingAccount sends time-locked coins from the input module account to the recipient
func (k Keeper) SendTimeLockedCoinsToPeriodicVestingAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins, length int64) error {
err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, senderModule, recipientAddr, amt)
if err != nil {
return err
}
k.addCoinsToVestingSchedule(ctx, recipientAddr, amt, length)
return nil
}
// SendTimeLockedCoinsToBaseAccount sends time-locked coins from the input module account to the recipient, converting the recipient account to a vesting account
func (k Keeper) SendTimeLockedCoinsToBaseAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins, length int64) error {
err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, senderModule, recipientAddr, amt)
if err != nil {
return err
}
acc := k.accountKeeper.GetAccount(ctx, recipientAddr)
// transition the account to a periodic vesting account:
bacc := authtypes.NewBaseAccount(acc.GetAddress(), acc.GetCoins(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence())
newPeriods := vesting.Periods{types.NewPeriod(amt, length)}
bva, err := vesting.NewBaseVestingAccount(bacc, amt, ctx.BlockTime().Unix()+length)
if err != nil {
return err
}
pva := vesting.NewPeriodicVestingAccountRaw(bva, ctx.BlockTime().Unix(), newPeriods)
k.accountKeeper.SetAccount(ctx, pva)
return nil
}
// DeleteExpiredClaimsAndClaimPeriods deletes expired claim periods and their associated claims
func (k Keeper) DeleteExpiredClaimsAndClaimPeriods(ctx sdk.Context) {
k.IterateClaimPeriods(ctx, func(cp types.ClaimPeriod) (stop bool) {
if !cp.End.Before(ctx.BlockTime()) {
return false
}
k.IterateClaims(ctx, func(c types.Claim) (stop bool) {
if !(c.Denom == cp.Denom && c.ClaimPeriodID == cp.ID) {
return false
}
k.DeleteClaim(ctx, c.Owner, c.Denom, c.ClaimPeriodID)
return false
})
k.DeleteClaimPeriod(ctx, cp.ID, cp.Denom)
return false
})
}
// GetClaimsByAddressAndDenom returns all claims for a specific user and address and a bool for if any were found
func (k Keeper) GetClaimsByAddressAndDenom(ctx sdk.Context, addr sdk.AccAddress, denom string) (claims types.Claims, found bool) {
found = false
k.IterateClaimPeriods(ctx, func(cp types.ClaimPeriod) (stop bool) {
if cp.Denom != denom {
return false
}
c, hasClaim := k.GetClaim(ctx, addr, cp.Denom, cp.ID)
if !hasClaim {
return false
}
found = true
claims = append(claims, c)
return false
})
return claims, found
}
// addCoinsToVestingSchedule adds coins to the input account's vesting schedule where length is the amount of time (from the current block time), in seconds, that the coins will be vesting for
// the input address must be a periodic vesting account
func (k Keeper) addCoinsToVestingSchedule(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins, length int64) {
acc := k.accountKeeper.GetAccount(ctx, addr)
vacc := acc.(*vesting.PeriodicVestingAccount)
// Add the new vesting coins to OriginalVesting
vacc.OriginalVesting = vacc.OriginalVesting.Add(amt...)
// update vesting periods
if vacc.EndTime < ctx.BlockTime().Unix() {
// edge case one - the vesting account's end time is in the past (ie, all previous vesting periods have completed)
// append a new period to the vesting account, update the end time, update the account in the store and return
newPeriodLength := (ctx.BlockTime().Unix() - vacc.EndTime) + length
newPeriod := types.NewPeriod(amt, newPeriodLength)
vacc.VestingPeriods = append(vacc.VestingPeriods, newPeriod)
vacc.EndTime = ctx.BlockTime().Unix() + length
k.accountKeeper.SetAccount(ctx, vacc)
return
}
if vacc.StartTime > ctx.BlockTime().Unix() {
// edge case two - the vesting account's start time is in the future (all periods have not started)
// update the start time to now and adjust the period lengths in place - a new period will be inserted in the next code block
updatedPeriods := vesting.Periods{}
for i, period := range vacc.VestingPeriods {
updatedPeriod := period
if i == 0 {
updatedPeriod = types.NewPeriod(period.Amount, (vacc.StartTime-ctx.BlockTime().Unix())+period.Length)
}
updatedPeriods = append(updatedPeriods, updatedPeriod)
}
vacc.VestingPeriods = updatedPeriods
vacc.StartTime = ctx.BlockTime().Unix()
}
// logic for inserting a new vesting period into the existing vesting schedule
totalPeriodLength := types.GetTotalVestingPeriodLength(vacc.VestingPeriods)
proposedEndTime := ctx.BlockTime().Unix() + length
if totalPeriodLength < length {
// in the case that the proposed length is longer than the sum of all previous period lengths, create a new period with length equal to the difference between the proposed length and the previous total length
newPeriodLength := length - totalPeriodLength
newPeriod := types.NewPeriod(amt, newPeriodLength)
vacc.VestingPeriods = append(vacc.VestingPeriods, newPeriod)
// update the end time so that the sum of all period lengths equals endTime - startTime
vacc.EndTime = proposedEndTime
} else {
// In the case that the proposed length is less than or equal to the sum of all previous period lengths, insert the period and update other periods as necessary.
// EXAMPLE (l is length, a is amount)
// Original Periods: {[l: 1 a: 1], [l: 2, a: 1], [l:8, a:3], [l: 5, a: 3]}
// Period we want to insert [l: 5, a: x]
// Expected result:
// {[l: 1, a: 1], [l:2, a: 1], [l:2, a:x], [l:6, a:3], [l:5, a:3]}
newPeriods := vesting.Periods{}
lengthCounter := int64(0)
appendRemaining := false
for _, period := range vacc.VestingPeriods {
if appendRemaining {
newPeriods = append(newPeriods, period)
continue
}
lengthCounter += period.Length
if lengthCounter < length {
newPeriods = append(newPeriods, period)
} else if lengthCounter == length {
newPeriod := types.NewPeriod(period.Amount.Add(amt...), period.Length)
newPeriods = append(newPeriods, newPeriod)
appendRemaining = true
} else {
newPeriod := types.NewPeriod(amt, length-types.GetTotalVestingPeriodLength(newPeriods))
previousPeriod := types.NewPeriod(period.Amount, period.Length-newPeriod.Length)
newPeriods = append(newPeriods, newPeriod, previousPeriod)
appendRemaining = true
}
}
vacc.VestingPeriods = newPeriods
}
k.accountKeeper.SetAccount(ctx, vacc)
return
}

View File

@ -0,0 +1,357 @@
package keeper_test
import (
"errors"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/incentive/types"
"github.com/kava-labs/kava/x/kavadist"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
abci "github.com/tendermint/tendermint/abci/types"
)
func (suite *KeeperTestSuite) setupChain() {
// creates a new app state with 4 funded addresses and 1 module account
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: time.Unix(100, 0)})
_, addrs := app.GeneratePrivKeyAddressPairs(4)
authGS := app.NewAuthGenState(
addrs,
[]sdk.Coins{
cs(c("ukava", 400)),
cs(c("ukava", 400)),
cs(c("ukava", 400)),
cs(c("ukava", 400)),
})
tApp.InitializeFromGenesisStates(
authGS,
)
supplyKeeper := tApp.GetSupplyKeeper()
macc := supplyKeeper.GetModuleAccount(ctx, kavadist.ModuleName)
err := supplyKeeper.MintCoins(ctx, macc.GetName(), cs(c("ukava", 500)))
suite.Require().NoError(err)
// sets addrs[0] to be a periodic vesting account
ak := tApp.GetAccountKeeper()
acc := ak.GetAccount(ctx, addrs[0])
bacc := auth.NewBaseAccount(acc.GetAddress(), acc.GetCoins(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence())
periods := vesting.Periods{
vesting.Period{Length: int64(1), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(8), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))},
}
bva, err2 := vesting.NewBaseVestingAccount(bacc, cs(c("ukava", 400)), ctx.BlockTime().Unix()+16)
suite.Require().NoError(err2)
pva := vesting.NewPeriodicVestingAccountRaw(bva, ctx.BlockTime().Unix(), periods)
ak.SetAccount(ctx, pva)
// sets addrs[2] to be a validator vesting account
acc = ak.GetAccount(ctx, addrs[2])
bacc = auth.NewBaseAccount(acc.GetAddress(), acc.GetCoins(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence())
bva, err2 = vesting.NewBaseVestingAccount(bacc, cs(c("ukava", 400)), ctx.BlockTime().Unix()+16)
suite.Require().NoError(err2)
vva := validatorvesting.NewValidatorVestingAccountRaw(bva, ctx.BlockTime().Unix(), periods, sdk.ConsAddress{}, nil, 90)
ak.SetAccount(ctx, vva)
suite.app = tApp
suite.keeper = tApp.GetIncentiveKeeper()
suite.ctx = ctx
suite.addrs = addrs
}
func (suite *KeeperTestSuite) setupExpiredClaims() {
// creates a new app state with 4 funded addresses
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: time.Unix(100, 0)})
_, addrs := app.GeneratePrivKeyAddressPairs(4)
authGS := app.NewAuthGenState(
addrs,
[]sdk.Coins{
cs(c("ukava", 400)),
cs(c("ukava", 400)),
cs(c("ukava", 400)),
cs(c("ukava", 400)),
})
tApp.InitializeFromGenesisStates(
authGS,
)
// creates two claim periods, one expired, and one that expires in the future
cp1 := types.NewClaimPeriod("bnb", 1, time.Unix(90, 0), time.Hour*8766)
cp2 := types.NewClaimPeriod("xrp", 1, time.Unix(110, 0), time.Hour*8766)
suite.keeper = tApp.GetIncentiveKeeper()
suite.keeper.SetClaimPeriod(ctx, cp1)
suite.keeper.SetClaimPeriod(ctx, cp2)
// creates one claim for the non-expired claim period and one claim for the expired claim period
c1 := types.NewClaim(addrs[0], c("ukava", 1000000), "bnb", 1)
c2 := types.NewClaim(addrs[0], c("ukava", 1000000), "xrp", 1)
suite.keeper.SetClaim(ctx, c1)
suite.keeper.SetClaim(ctx, c2)
suite.app = tApp
suite.ctx = ctx
suite.addrs = addrs
}
func (suite *KeeperTestSuite) TestSendCoinsToPeriodicVestingAccount() {
suite.setupChain()
type args struct {
coins sdk.Coins
length int64
}
type errArgs struct {
expectErr bool
errType error
}
type vestingAccountTest struct {
name string
blockTime time.Time
args args
errArgs errArgs
expectedPeriods vesting.Periods
expectedOriginalVesting sdk.Coins
expectedCoins sdk.Coins
expectedStartTime int64
expectedEndTime int64
}
type vestingAccountTests []vestingAccountTest
testCases := vestingAccountTests{
vestingAccountTest{
name: "insert period into an existing vesting schedule",
blockTime: time.Unix(100, 0),
args: args{coins: cs(c("ukava", 100)), length: 5},
errArgs: errArgs{expectErr: false, errType: nil},
expectedPeriods: vesting.Periods{
vesting.Period{Length: int64(1), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(6), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))},
},
expectedOriginalVesting: cs(c("ukava", 500)),
expectedCoins: cs(c("ukava", 500)),
expectedStartTime: int64(100),
expectedEndTime: int64(116),
},
vestingAccountTest{
name: "append period to the end of an existing vesting schedule",
blockTime: time.Unix(100, 0),
args: args{coins: cs(c("ukava", 100)), length: 17},
errArgs: errArgs{expectErr: false, errType: nil},
expectedPeriods: vesting.Periods{
vesting.Period{Length: int64(1), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(6), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(1), Amount: cs(c("ukava", 100))},
},
expectedOriginalVesting: cs(c("ukava", 600)),
expectedCoins: cs(c("ukava", 600)),
expectedStartTime: int64(100),
expectedEndTime: int64(117),
},
vestingAccountTest{
name: "append period to the end of a completed vesting schedule",
blockTime: time.Unix(120, 0),
args: args{coins: cs(c("ukava", 100)), length: 5},
errArgs: errArgs{expectErr: false, errType: nil},
expectedPeriods: vesting.Periods{
vesting.Period{Length: int64(1), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(6), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(1), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(8), Amount: cs(c("ukava", 100))},
},
expectedOriginalVesting: cs(c("ukava", 700)),
expectedCoins: cs(c("ukava", 700)),
expectedStartTime: int64(100),
expectedEndTime: int64(125),
},
vestingAccountTest{
name: "prepend period to to an upcoming vesting schedule",
blockTime: time.Unix(90, 0),
args: args{coins: cs(c("ukava", 100)), length: 5},
errArgs: errArgs{expectErr: false, errType: nil},
expectedPeriods: vesting.Periods{
vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(6), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(6), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(1), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(8), Amount: cs(c("ukava", 100))},
},
expectedOriginalVesting: cs(c("ukava", 800)),
expectedCoins: cs(c("ukava", 800)),
expectedStartTime: int64(90),
expectedEndTime: int64(125),
},
vestingAccountTest{
name: "add period that coincides with an existing end time",
blockTime: time.Unix(90, 0),
args: args{coins: cs(c("ukava", 100)), length: 11},
errArgs: errArgs{expectErr: false, errType: nil},
expectedPeriods: vesting.Periods{
vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(6), Amount: cs(c("ukava", 200))},
vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(6), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(1), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(8), Amount: cs(c("ukava", 100))},
},
expectedOriginalVesting: cs(c("ukava", 900)),
expectedCoins: cs(c("ukava", 900)),
expectedStartTime: int64(90),
expectedEndTime: int64(125),
},
vestingAccountTest{
name: "insufficient module account balance",
blockTime: time.Unix(90, 0),
args: args{coins: cs(c("ukava", 1000)), length: 11},
errArgs: errArgs{expectErr: true, errType: types.ErrInsufficientModAccountBalance},
expectedPeriods: vesting.Periods{},
expectedOriginalVesting: sdk.Coins{},
expectedCoins: sdk.Coins{},
expectedStartTime: int64(0),
expectedEndTime: int64(0),
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.ctx = suite.ctx.WithBlockTime(tc.blockTime)
err := suite.keeper.SendTimeLockedCoinsToAccount(suite.ctx, kavadist.ModuleName, suite.addrs[0], tc.args.coins, tc.args.length)
if tc.errArgs.expectErr {
suite.Require().True(errors.Is(err, tc.errArgs.errType))
} else {
suite.Require().NoError(err)
acc := suite.getAccount(suite.addrs[0])
vacc, ok := acc.(*vesting.PeriodicVestingAccount)
suite.True(ok)
suite.Equal(tc.expectedPeriods, vacc.VestingPeriods)
suite.Equal(tc.expectedOriginalVesting, vacc.OriginalVesting)
suite.Equal(tc.expectedCoins, vacc.Coins)
suite.Equal(tc.expectedStartTime, vacc.StartTime)
suite.Equal(tc.expectedEndTime, vacc.EndTime)
}
})
}
}
func (suite *KeeperTestSuite) TestSendCoinsToBaseAccount() {
suite.setupChain()
// send coins to base account
err := suite.keeper.SendTimeLockedCoinsToAccount(suite.ctx, kavadist.ModuleName, suite.addrs[1], cs(c("ukava", 100)), 5)
suite.Require().NoError(err)
acc := suite.getAccount(suite.addrs[1])
vacc, ok := acc.(*vesting.PeriodicVestingAccount)
suite.True(ok)
expectedPeriods := vesting.Periods{
vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))},
}
suite.Equal(expectedPeriods, vacc.VestingPeriods)
suite.Equal(cs(c("ukava", 100)), vacc.OriginalVesting)
suite.Equal(cs(c("ukava", 500)), vacc.Coins)
suite.Equal(int64(105), vacc.EndTime)
suite.Equal(int64(100), vacc.StartTime)
}
func (suite *KeeperTestSuite) TestSendCoinsToInvalidAccount() {
suite.setupChain()
err := suite.keeper.SendTimeLockedCoinsToAccount(suite.ctx, kavadist.ModuleName, suite.addrs[2], cs(c("ukava", 100)), 5)
suite.Require().True(errors.Is(err, types.ErrInvalidAccountType))
macc := suite.getModuleAccount(cdp.ModuleName)
err = suite.keeper.SendTimeLockedCoinsToAccount(suite.ctx, kavadist.ModuleName, macc.GetAddress(), cs(c("ukava", 100)), 5)
suite.Require().True(errors.Is(err, types.ErrInvalidAccountType))
}
func (suite *KeeperTestSuite) TestPayoutClaim() {
suite.setupChain() // adds 3 accounts - 1 periodic vesting account, 1 base account, and 1 validator vesting account
// add 2 claims that correspond to an existing claim period and one claim that has no corresponding claim period
cp1 := types.NewClaimPeriod("bnb", 1, suite.ctx.BlockTime().Add(time.Hour*168), time.Hour*8766)
suite.keeper.SetClaimPeriod(suite.ctx, cp1)
// valid claim for addrs[0]
c1 := types.NewClaim(suite.addrs[0], c("ukava", 100), "bnb", 1)
// invalid claim for addrs[0]
c2 := types.NewClaim(suite.addrs[0], c("ukava", 100), "xrp", 1)
// valid claim for addrs[1]
c3 := types.NewClaim(suite.addrs[1], c("ukava", 100), "bnb", 1)
suite.keeper.SetClaim(suite.ctx, c1)
suite.keeper.SetClaim(suite.ctx, c2)
suite.keeper.SetClaim(suite.ctx, c3)
// existing claim with corresponding claim period successfully claimed by existing periodic vesting account
err := suite.keeper.PayoutClaim(suite.ctx, suite.addrs[0], "bnb", 1)
suite.Require().NoError(err)
acc := suite.getAccount(suite.addrs[0])
// account is a periodic vesting account
vacc, ok := acc.(*vesting.PeriodicVestingAccount)
suite.True(ok)
// vesting balance is correct
suite.Equal(cs(c("ukava", 500)), vacc.OriginalVesting)
// existing claim with corresponding claim period successfully claimed by base account
err = suite.keeper.PayoutClaim(suite.ctx, suite.addrs[1], "bnb", 1)
suite.Require().NoError(err)
acc = suite.getAccount(suite.addrs[1])
// account has become a periodic vesting account
vacc, ok = acc.(*vesting.PeriodicVestingAccount)
suite.True(ok)
// vesting balance is correct
suite.Equal(cs(c("ukava", 100)), vacc.OriginalVesting)
// addrs[3] has no claims
err = suite.keeper.PayoutClaim(suite.ctx, suite.addrs[3], "bnb", 1)
suite.Require().True(errors.Is(err, types.ErrClaimNotFound))
// addrs[0] has an xrp claim, but there is not corresponding claim period
err = suite.keeper.PayoutClaim(suite.ctx, suite.addrs[0], "xrp", 1)
suite.Require().True(errors.Is(err, types.ErrClaimPeriodNotFound))
}
func (suite *KeeperTestSuite) TestDeleteExpiredClaimPeriods() {
suite.setupExpiredClaims() // creates new app state with one non-expired claim period (xrp) and one expired claim period (bnb) as well as a claim that corresponds to each claim period
// both claim periods are present
_, found := suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
suite.True(found)
_, found = suite.keeper.GetClaimPeriod(suite.ctx, 1, "xrp")
suite.True(found)
// both claims are present
_, found = suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "bnb", 1)
suite.True(found)
_, found = suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "xrp", 1)
suite.True(found)
// expired claim period and associated claims should get deleted
suite.NotPanics(func() {
suite.keeper.DeleteExpiredClaimsAndClaimPeriods(suite.ctx)
})
// expired claim period and claim are not found
_, found = suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
suite.False(found)
_, found = suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "bnb", 1)
suite.False(found)
// non-expired claim period and claim are found
_, found = suite.keeper.GetClaimPeriod(suite.ctx, 1, "xrp")
suite.True(found)
_, found = suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "xrp", 1)
suite.True(found)
}

View File

@ -0,0 +1,51 @@
package keeper
import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/incentive/types"
abci "github.com/tendermint/tendermint/abci/types"
)
// NewQuerier is the module level router for state queries
func NewQuerier(k Keeper) sdk.Querier {
return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err error) {
switch path[0] {
case types.QueryGetParams:
return queryGetParams(ctx, req, k)
case types.QueryGetClaims:
return queryGetClaims(ctx, req, k)
default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName)
}
}
}
// query params in the store
func queryGetParams(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
// Get params
params := k.GetParams(ctx)
// Encode results
bz, err := codec.MarshalJSONIndent(k.cdc, params)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
func queryGetClaims(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
var requestParams types.QueryClaimsParams
err := k.cdc.UnmarshalJSON(req.Data, &requestParams)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
claims, _ := k.GetClaimsByAddressAndDenom(ctx, requestParams.Owner, requestParams.Denom)
bz, err := codec.MarshalJSONIndent(k.cdc, claims)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}

View File

@ -0,0 +1,32 @@
package keeper_test
import (
"strings"
"github.com/kava-labs/kava/x/incentive/keeper"
"github.com/kava-labs/kava/x/incentive/types"
abci "github.com/tendermint/tendermint/abci/types"
)
func (suite *KeeperTestSuite) TestQuerier() {
suite.addObjectsToStore()
querier := keeper.NewQuerier(suite.keeper)
bz, err := querier(suite.ctx, []string{types.QueryGetParams}, abci.RequestQuery{})
suite.Require().NoError(err)
suite.NotNil(bz)
var p types.Params
suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &p))
claimQueryParams := types.NewQueryClaimsParams(suite.addrs[0], "bnb")
query := abci.RequestQuery{
Path: strings.Join([]string{"custom", types.QuerierRoute, types.QueryGetClaims}, "/"),
Data: types.ModuleCdc.MustMarshalJSON(claimQueryParams),
}
bz, err = querier(suite.ctx, []string{types.QueryGetClaims}, query)
var claims types.Claims
suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &claims))
suite.Equal(1, len(claims))
suite.Equal(types.Claims{types.NewClaim(suite.addrs[0], c("ukava", 1000000), "bnb", 1)}, claims)
}

View File

@ -0,0 +1,110 @@
package keeper
import (
"time"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
"github.com/kava-labs/kava/x/incentive/types"
)
// HandleRewardPeriodExpiry deletes expired RewardPeriods from the store and creates a ClaimPeriod in the store for each expired RewardPeriod
func (k Keeper) HandleRewardPeriodExpiry(ctx sdk.Context, rp types.RewardPeriod) {
k.CreateUniqueClaimPeriod(ctx, rp.Denom, rp.ClaimEnd, rp.ClaimTimeLock)
store := prefix.NewStore(ctx.KVStore(k.key), types.RewardPeriodKeyPrefix)
store.Delete([]byte(rp.Denom))
return
}
// CreateNewRewardPeriod creates a new reward period from the input reward
func (k Keeper) CreateNewRewardPeriod(ctx sdk.Context, reward types.Reward) {
// reward periods store the amount of rewards payed PER SECOND
rewardsPerSecond := sdk.NewDecFromInt(reward.AvailableRewards.Amount).Quo(sdk.NewDecFromInt(sdk.NewInt(int64(reward.Duration.Seconds())))).TruncateInt()
rewardCoinPerSecond := sdk.NewCoin(reward.AvailableRewards.Denom, rewardsPerSecond)
rp := types.RewardPeriod{
Denom: reward.Denom,
Start: ctx.BlockTime(),
End: ctx.BlockTime().Add(reward.Duration),
Reward: rewardCoinPerSecond,
ClaimEnd: ctx.BlockTime().Add(reward.Duration).Add(reward.ClaimDuration),
ClaimTimeLock: reward.TimeLock,
}
k.SetRewardPeriod(ctx, rp)
}
// CreateAndDeleteRewardPeriods creates reward periods for active rewards that don't already have a reward period and deletes reward periods for inactive rewards that currently have a reward period
func (k Keeper) CreateAndDeleteRewardPeriods(ctx sdk.Context) {
params := k.GetParams(ctx)
for _, r := range params.Rewards {
_, found := k.GetRewardPeriod(ctx, r.Denom)
// if governance has made a reward inactive, delete the current period
if found && !r.Active {
k.DeleteRewardPeriod(ctx, r.Denom)
}
// if a reward period for an active reward is not found, create one
if !found && r.Active {
k.CreateNewRewardPeriod(ctx, r)
}
}
}
// ApplyRewardsToCdps iterates over the reward periods and creates a claim for each cdp owner that created usdx with the collateral specified in the reward period
func (k Keeper) ApplyRewardsToCdps(ctx sdk.Context) {
previousBlockTime, found := k.GetPreviousBlockTime(ctx)
if !found {
previousBlockTime = ctx.BlockTime()
k.SetPreviousBlockTime(ctx, previousBlockTime)
return
}
k.IterateRewardPeriods(ctx, func(rp types.RewardPeriod) bool {
expired := false
// the total amount of usdx created with the collateral type being incentivized
totalPrincipal := k.cdpKeeper.GetTotalPrincipal(ctx, rp.Denom, types.PrincipalDenom)
// the number of seconds since last payout
timeElapsed := sdk.NewInt(ctx.BlockTime().Unix() - previousBlockTime.Unix())
if rp.End.Before(ctx.BlockTime()) {
timeElapsed = sdk.NewInt(rp.End.Unix() - previousBlockTime.Unix())
expired = true
}
// the amount of rewards to pay (rewardAmount * timeElapsed)
rewardsThisPeriod := rp.Reward.Amount.Mul(timeElapsed)
id := k.GetNextClaimPeriodID(ctx, rp.Denom)
k.cdpKeeper.IterateCdpsByDenom(ctx, rp.Denom, func(cdp cdptypes.CDP) bool {
rewardsShare := sdk.NewDecFromInt(cdp.Principal.Amount.Add(cdp.AccumulatedFees.Amount)).Quo(sdk.NewDecFromInt(totalPrincipal))
// sanity check - don't create zero claims
if rewardsShare.IsZero() {
return false
}
rewardsEarned := rewardsShare.Mul(sdk.NewDecFromInt(rewardsThisPeriod)).RoundInt()
k.AddToClaim(ctx, cdp.Owner, rp.Denom, id, sdk.NewCoin(types.GovDenom, rewardsEarned))
return false
})
if !expired {
return false
}
k.HandleRewardPeriodExpiry(ctx, rp)
return false
})
k.SetPreviousBlockTime(ctx, ctx.BlockTime())
}
// CreateUniqueClaimPeriod creates a new claim period in the store and updates the highest claim period id
func (k Keeper) CreateUniqueClaimPeriod(ctx sdk.Context, denom string, end time.Time, timeLock time.Duration) {
id := k.GetNextClaimPeriodID(ctx, denom)
claimPeriod := types.NewClaimPeriod(denom, id, end, timeLock)
k.SetClaimPeriod(ctx, claimPeriod)
k.SetNextClaimPeriodID(ctx, denom, id+1)
}
// AddToClaim adds the amount to an existing claim or creates a new one for that amount
func (k Keeper) AddToClaim(ctx sdk.Context, addr sdk.AccAddress, denom string, id uint64, amount sdk.Coin) {
claim, found := k.GetClaim(ctx, addr, denom, id)
if found {
claim.Reward = claim.Reward.Add(amount)
} else {
claim = types.NewClaim(addr, amount, denom, id)
}
k.SetClaim(ctx, claim)
}

View File

@ -0,0 +1,256 @@
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/incentive/types"
"github.com/kava-labs/kava/x/pricefeed"
abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
)
func (suite *KeeperTestSuite) TestExpireRewardPeriod() {
rp := types.NewRewardPeriod("bnb", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), time.Hour*8766)
suite.keeper.SetRewardPeriod(suite.ctx, rp)
suite.keeper.SetNextClaimPeriodID(suite.ctx, "bnb", 1)
suite.NotPanics(func() {
suite.keeper.HandleRewardPeriodExpiry(suite.ctx, rp)
})
_, found := suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
suite.True(found)
}
func (suite *KeeperTestSuite) TestAddToClaim() {
rp := types.NewRewardPeriod("bnb", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), time.Hour*8766)
suite.keeper.SetRewardPeriod(suite.ctx, rp)
suite.keeper.SetNextClaimPeriodID(suite.ctx, "bnb", 1)
suite.keeper.HandleRewardPeriodExpiry(suite.ctx, rp)
c1 := types.NewClaim(suite.addrs[0], c("ukava", 1000000), "bnb", 1)
suite.keeper.SetClaim(suite.ctx, c1)
suite.NotPanics(func() {
suite.keeper.AddToClaim(suite.ctx, suite.addrs[0], "bnb", 1, c("ukava", 1000000))
})
testC, _ := suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "bnb", 1)
suite.Equal(c("ukava", 2000000), testC.Reward)
suite.NotPanics(func() {
suite.keeper.AddToClaim(suite.ctx, suite.addrs[0], "xpr", 1, c("ukava", 1000000))
})
}
func (suite *KeeperTestSuite) TestCreateRewardPeriod() {
reward := types.NewReward(true, "bnb", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24)
suite.NotPanics(func() {
suite.keeper.CreateNewRewardPeriod(suite.ctx, reward)
})
_, found := suite.keeper.GetRewardPeriod(suite.ctx, "bnb")
suite.True(found)
}
func (suite *KeeperTestSuite) TestCreateAndDeleteRewardsPeriods() {
reward1 := types.NewReward(true, "bnb", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24)
reward2 := types.NewReward(false, "xrp", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24)
reward3 := types.NewReward(false, "btc", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24)
// add a reward period to the store for a non-active reward
suite.NotPanics(func() {
suite.keeper.CreateNewRewardPeriod(suite.ctx, reward3)
})
params := types.NewParams(true, types.Rewards{reward1, reward2, reward3})
suite.keeper.SetParams(suite.ctx, params)
suite.NotPanics(func() {
suite.keeper.CreateAndDeleteRewardPeriods(suite.ctx)
})
testCases := []struct {
name string
arg string
expectFound bool
}{
{
"active reward period",
"bnb",
true,
},
{
"attempt to add inactive reward period",
"xrp",
false,
},
{
"remove inactive reward period",
"btc",
false,
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
_, found := suite.keeper.GetRewardPeriod(suite.ctx, tc.arg)
if tc.expectFound {
suite.True(found)
} else {
suite.False(found)
}
})
}
}
func (suite *KeeperTestSuite) TestApplyRewardsToCdps() {
suite.setupCdpChain() // creates a test app with 3 BNB cdps and usdx incentives for bnb - each reward period is one week
// move the context forward by 100 periods
suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Second * 100))
// apply rewards to BNB cdps
suite.NotPanics(func() {
suite.keeper.ApplyRewardsToCdps(suite.ctx)
})
// each cdp should have a claim
claims := types.Claims{}
suite.keeper.IterateClaims(suite.ctx, func(c types.Claim) (stop bool) {
claims = append(claims, c)
return false
})
suite.Equal(3, len(claims))
// there should be no associated claim period, because the reward period has not ended yet
_, found := suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
suite.False(found)
// move ctx to the reward period expiry and check that the claim period has been created and the next claim period id has increased
suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour * 24 * 7))
suite.NotPanics(func() {
// apply rewards to cdps
suite.keeper.ApplyRewardsToCdps(suite.ctx)
// delete the old reward period amd create a new one
suite.keeper.CreateAndDeleteRewardPeriods(suite.ctx)
})
_, found = suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
suite.True(found)
testID := suite.keeper.GetNextClaimPeriodID(suite.ctx, "bnb")
suite.Equal(uint64(2), testID)
// move the context forward by 100 periods
suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Second * 100))
// run the begin blocker functions
suite.NotPanics(func() {
suite.keeper.DeleteExpiredClaimsAndClaimPeriods(suite.ctx)
suite.keeper.ApplyRewardsToCdps(suite.ctx)
suite.keeper.CreateAndDeleteRewardPeriods(suite.ctx)
})
// each cdp should now have two claims
claims = types.Claims{}
suite.keeper.IterateClaims(suite.ctx, func(c types.Claim) (stop bool) {
claims = append(claims, c)
return false
})
suite.Equal(6, len(claims))
}
func (suite *KeeperTestSuite) setupCdpChain() {
// creates a new test app with bnb as the only asset the pricefeed and cdp modules
// funds three addresses and creates 3 cdps, funded with 100 BNB, 1000 BNB, and 10000 BNB
// each CDP draws 10, 100, and 1000 USDX respectively
// adds usdx incentives for bnb - 1000 KAVA per week with a 1 year time lock
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
// need pricefeed and cdp gen state with one collateral
pricefeedGS := pricefeed.GenesisState{
Params: pricefeed.Params{
Markets: []pricefeed.Market{
pricefeed.Market{MarketID: "bnb:usd", BaseAsset: "bnb", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
},
},
PostedPrices: []pricefeed.PostedPrice{
pricefeed.PostedPrice{
MarketID: "bnb:usd",
OracleAddress: sdk.AccAddress{},
Price: d("12.29"),
Expiry: time.Now().Add(100000 * time.Hour),
},
},
}
// need incentive params for one collateral
cdpGS := cdp.GenesisState{
Params: cdp.Params{
GlobalDebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
DebtAuctionThreshold: cdp.DefaultDebtThreshold,
SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
CollateralParams: cdp.CollateralParams{
{
Denom: "bnb",
LiquidationRatio: sdk.MustNewDecFromStr("2.0"),
DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr
LiquidationPenalty: d("0.05"),
AuctionSize: i(10000000000),
Prefix: 0x20,
MarketID: "bnb:usd",
ConversionFactor: i(8),
},
},
DebtParam: cdp.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
SavingsRate: d("0.95"),
},
},
StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom,
GovDenom: cdp.DefaultGovDenom,
CDPs: cdp.CDPs{},
PreviousDistributionTime: cdp.DefaultPreviousDistributionTime,
}
incentiveGS := types.NewGenesisState(
types.NewParams(
true, types.Rewards{types.NewReward(true, "bnb", c("ukava", 1000000000), time.Hour*7*24, time.Hour*24*365, time.Hour*7*24)},
),
types.DefaultPreviousBlockTime,
types.RewardPeriods{types.NewRewardPeriod("bnb", ctx.BlockTime(), ctx.BlockTime().Add(time.Hour*7*24), c("ukava", 1000), ctx.BlockTime().Add(time.Hour*7*24*2), time.Hour*365*24)},
types.ClaimPeriods{},
types.Claims{},
types.GenesisClaimPeriodIDs{})
pricefeedAppGs := app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)}
cdpAppGs := app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGS)}
incentiveAppGs := app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(incentiveGS)}
_, addrs := app.GeneratePrivKeyAddressPairs(3)
authGS := app.NewAuthGenState(
addrs[0:3],
[]sdk.Coins{
cs(c("bnb", 10000000000)),
cs(c("bnb", 100000000000)),
cs(c("bnb", 1000000000000)),
})
tApp.InitializeFromGenesisStates(
authGS,
pricefeedAppGs,
incentiveAppGs,
cdpAppGs,
)
suite.app = tApp
suite.keeper = tApp.GetIncentiveKeeper()
suite.ctx = ctx
// create 3 cdps
cdpKeeper := tApp.GetCDPKeeper()
err := cdpKeeper.AddCdp(suite.ctx, addrs[0], c("bnb", 10000000000), c("usdx", 10000000))
suite.Require().NoError(err)
err = cdpKeeper.AddCdp(suite.ctx, addrs[1], c("bnb", 100000000000), c("usdx", 100000000))
suite.Require().NoError(err)
err = cdpKeeper.AddCdp(suite.ctx, addrs[2], c("bnb", 1000000000000), c("usdx", 1000000000))
suite.Require().NoError(err)
// total usd is 1110
// set the previous block time
suite.keeper.SetPreviousBlockTime(suite.ctx, suite.ctx.BlockTime())
}
// 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...) }

167
x/incentive/module.go Normal file
View File

@ -0,0 +1,167 @@
package incentive
import (
"encoding/json"
"math/rand"
"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"
"github.com/cosmos/cosmos-sdk/x/auth"
sim "github.com/cosmos/cosmos-sdk/x/simulation"
"github.com/gorilla/mux"
"github.com/kava-labs/kava/x/incentive/client/cli"
"github.com/kava-labs/kava/x/incentive/client/rest"
"github.com/kava-labs/kava/x/incentive/keeper"
"github.com/kava-labs/kava/x/incentive/simulation"
"github.com/kava-labs/kava/x/incentive/types"
"github.com/spf13/cobra"
abci "github.com/tendermint/tendermint/abci/types"
)
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
_ module.AppModuleSimulation = AppModule{}
)
// AppModuleBasic defines the basic application module used by the incentive module.
type AppModuleBasic struct{}
// Name returns the incentive module's name.
func (AppModuleBasic) Name() string {
return types.ModuleName
}
// RegisterCodec registers the incentive module's types for the given codec.
func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) {
types.RegisterCodec(cdc)
}
// DefaultGenesis returns default genesis state as raw bytes for the incentive
// module.
func (AppModuleBasic) DefaultGenesis() json.RawMessage {
return types.ModuleCdc.MustMarshalJSON(types.DefaultGenesisState())
}
// ValidateGenesis performs genesis state validation for the incentive module.
func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
var gs types.GenesisState
err := types.ModuleCdc.UnmarshalJSON(bz, &gs)
if err != nil {
return err
}
return gs.Validate()
}
// RegisterRESTRoutes registers the REST routes for the incentive module.
func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) {
rest.RegisterRoutes(ctx, rtr)
}
// GetTxCmd returns the root tx command for the incentive module.
func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetTxCmd(cdc)
}
// GetQueryCmd returns no root query command for the crisis module.
func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetQueryCmd(types.StoreKey, cdc)
}
// RegisterStoreDecoder registers a decoder for cdp module's types
func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
sdr[StoreKey] = simulation.DecodeStore
}
// GenerateGenesisState creates a randomized GenState of the cdp module
func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) {
simulation.RandomizedGenState(simState)
}
// RandomizedParams creates randomized cdp param changes for the simulator.
func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange {
return simulation.ParamChanges(r)
}
// ProposalContents doesn't return any content functions for governance proposals.
func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent {
return nil
}
// WeightedOperations returns the all the bep3 module operations with their respective weights.
func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation {
return simulation.WeightedOperations(simState.AppParams, simState.Cdc, am.accountKeeper, am.supplyKeeper, am.keeper)
}
// AppModule implements the sdk.AppModule interface.
type AppModule struct {
AppModuleBasic
keeper Keeper
accountKeeper auth.AccountKeeper
supplyKeeper SupplyKeeper
}
// NewAppModule creates a new AppModule object
func NewAppModule(keeper Keeper, accountKeeper auth.AccountKeeper, supplyKeeper SupplyKeeper) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{},
keeper: keeper,
accountKeeper: accountKeeper,
supplyKeeper: supplyKeeper,
}
}
// Name returns the incentive module's name.
func (AppModule) Name() string {
return types.ModuleName
}
// RegisterInvariants registers the incentive module invariants.
func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
// Route returns the message routing key for the incentive module.
func (AppModule) Route() string {
return types.RouterKey
}
// NewHandler returns an sdk.Handler for the incentive module.
func (am AppModule) NewHandler() sdk.Handler {
return NewHandler(am.keeper)
}
// QuerierRoute returns the incentive module's querier route name.
func (AppModule) QuerierRoute() string {
return types.QuerierRoute
}
// NewQuerierHandler returns the incentive module sdk.Querier.
func (am AppModule) NewQuerierHandler() sdk.Querier {
return keeper.NewQuerier(am.keeper)
}
// InitGenesis performs genesis initialization for the incentive module. It returns no validator updates.
func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate {
var gs types.GenesisState
types.ModuleCdc.MustUnmarshalJSON(data, &gs)
InitGenesis(ctx, am.keeper, am.supplyKeeper, gs)
return []abci.ValidatorUpdate{}
}
// ExportGenesis returns the exported genesis state as raw bytes for the incentive module
func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage {
gs := ExportGenesis(ctx, am.keeper)
return types.ModuleCdc.MustMarshalJSON(gs)
}
// BeginBlock returns the begin blocker for the incentive module.
func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) {
BeginBlocker(ctx, am.keeper)
}
// EndBlock returns the end blocker for the incentive module. It returns no validator updates.
func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{}
}

View File

@ -0,0 +1,50 @@
package simulation
import (
"bytes"
"encoding/binary"
"fmt"
"time"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/tendermint/tendermint/libs/kv"
"github.com/kava-labs/kava/x/incentive/types"
)
// DecodeStore unmarshals the KVPair's Value to the module's corresponding type
func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
switch {
case bytes.Equal(kvA.Key[:1], types.RewardPeriodKeyPrefix):
var rewardPeriodA, rewardPeriodB types.RewardPeriod
cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &rewardPeriodA)
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &rewardPeriodB)
return fmt.Sprintf("%v\n%v", rewardPeriodA, rewardPeriodB)
case bytes.Equal(kvA.Key[:1], types.ClaimPeriodKeyPrefix):
var claimPeriodA, claimPeriodB types.ClaimPeriod
cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &claimPeriodA)
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &claimPeriodB)
return fmt.Sprintf("%v\n%v", claimPeriodA, claimPeriodB)
case bytes.Equal(kvA.Key[:1], types.ClaimKeyPrefix):
var claimA, claimB types.Claim
cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &claimA)
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &claimB)
return fmt.Sprintf("%v\n%v", claimA, claimB)
case bytes.Equal(kvA.Key[:1], types.NextClaimPeriodIDPrefix):
claimPeriodIDA := binary.BigEndian.Uint64(kvA.Value)
claimPeriodIDB := binary.BigEndian.Uint64(kvB.Value)
return fmt.Sprintf("%d\n%d", claimPeriodIDA, claimPeriodIDB)
case bytes.Equal(kvA.Key[:1], types.PreviousBlockTimeKey):
var timeA, timeB time.Time
cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &timeA)
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &timeB)
return fmt.Sprintf("%s\n%s", timeA, timeB)
default:
panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1]))
}
}

View File

@ -0,0 +1,65 @@
package simulation
import (
"fmt"
"testing"
"time"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/libs/kv"
"github.com/kava-labs/kava/x/incentive/types"
)
func makeTestCodec() (cdc *codec.Codec) {
cdc = codec.New()
sdk.RegisterCodec(cdc)
types.RegisterCodec(cdc)
return
}
func TestDecodeDistributionStore(t *testing.T) {
cdc := makeTestCodec()
// Set up RewardPeriod, ClaimPeriod, Claim, and previous block time
rewardPeriod := types.NewRewardPeriod("btc", time.Now().UTC(), time.Now().Add(time.Hour*1).UTC(),
sdk.NewCoin("ukava", sdk.NewInt(10000000000)), time.Now().Add(time.Hour*2).UTC(), time.Duration(time.Hour*2))
claimPeriod := types.NewClaimPeriod("btc", 1, time.Now().Add(time.Hour*24).UTC(), time.Duration(time.Hour*24))
addr, _ := sdk.AccAddressFromBech32("kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw")
claim := types.NewClaim(addr, sdk.NewCoin("ukava", sdk.NewInt(1000000)), "bnb", 1)
prevBlockTime := time.Now().Add(time.Hour * -1).UTC()
kvPairs := kv.Pairs{
kv.Pair{Key: types.RewardPeriodKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(&rewardPeriod)},
kv.Pair{Key: types.ClaimPeriodKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(&claimPeriod)},
kv.Pair{Key: types.ClaimKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(&claim)},
kv.Pair{Key: types.NextClaimPeriodIDPrefix, Value: sdk.Uint64ToBigEndian(10)},
kv.Pair{Key: []byte(types.PreviousBlockTimeKey), Value: cdc.MustMarshalBinaryLengthPrefixed(prevBlockTime)},
kv.Pair{Key: []byte{0x99}, Value: []byte{0x99}},
}
tests := []struct {
name string
expectedLog string
}{
{"RewardPeriod", fmt.Sprintf("%v\n%v", rewardPeriod, rewardPeriod)},
{"ClaimPeriod", fmt.Sprintf("%v\n%v", claimPeriod, claimPeriod)},
{"Claim", fmt.Sprintf("%v\n%v", claim, claim)},
{"NextClaimPeriodID", "10\n10"},
{"PreviousBlockTime", fmt.Sprintf("%v\n%v", prevBlockTime, prevBlockTime)},
{"other", ""},
}
for i, tt := range tests {
i, tt := i, tt
t.Run(tt.name, func(t *testing.T) {
switch i {
case len(tests) - 1:
require.Panics(t, func() { DecodeStore(cdc, kvPairs[i], kvPairs[i]) }, tt.name)
default:
require.Equal(t, tt.expectedLog, DecodeStore(cdc, kvPairs[i], kvPairs[i]), tt.name)
}
})
}
}

View File

@ -0,0 +1,140 @@
package simulation
import (
"fmt"
"math/rand"
"time"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/simulation"
"github.com/kava-labs/kava/x/incentive/types"
)
var (
CollateralDenoms = [3]string{"bnb", "xrp", "btc"}
RewardDenom = "ukava"
MaxTotalAssetReward = sdk.NewInt(1000000000)
)
// RandomizedGenState generates a random GenesisState for incentive module
func RandomizedGenState(simState *module.SimulationState) {
params := genParams(simState.Rand)
rewardPeriods := genRewardPeriods(simState.Rand, simState.GenTimestamp, params.Rewards)
claimPeriods := genClaimPeriods(rewardPeriods)
claimPeriodIDs := genNextClaimPeriodIds(claimPeriods)
// New genesis state holds valid, linked reward periods, claim periods, and claim period IDs
incentiveGenesis := types.NewGenesisState(params, types.DefaultPreviousBlockTime,
rewardPeriods, claimPeriods, types.Claims{}, claimPeriodIDs)
if err := incentiveGenesis.Validate(); err != nil {
panic(err)
}
fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, incentiveGenesis))
simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(incentiveGenesis)
}
// genParams generates random rewards and is active by default
func genParams(r *rand.Rand) types.Params {
params := types.NewParams(true, genRewards(r))
if err := params.Validate(); err != nil {
panic(err)
}
return params
}
// genRewards generates rewards for each specified collateral type
func genRewards(r *rand.Rand) types.Rewards {
var rewards types.Rewards
for _, denom := range CollateralDenoms {
active := true
// total reward is in range (half max total reward, max total reward)
amount := simulation.RandIntBetween(r, int(MaxTotalAssetReward.Int64()/2), int(MaxTotalAssetReward.Int64()))
totalRewards := sdk.NewInt64Coin(RewardDenom, int64(amount))
// generate a random number of hours between 6-48 to use for reward's times
numbHours := simulation.RandIntBetween(r, 6, 48)
duration := time.Duration(time.Hour * time.Duration(numbHours))
timeLock := time.Duration(time.Hour * time.Duration(numbHours/2)) // half as long as duration
claimDuration := time.Hour * time.Duration(numbHours*2) // twice as long as duration
reward := types.NewReward(active, denom, totalRewards, duration, timeLock, claimDuration)
rewards = append(rewards, reward)
}
return rewards
}
// genRewardPeriods generates chronological reward periods for each given reward type
func genRewardPeriods(r *rand.Rand, timestamp time.Time, rewards types.Rewards) types.RewardPeriods {
var rewardPeriods types.RewardPeriods
for _, reward := range rewards {
rewardPeriodStart := timestamp
for i := 10; i >= simulation.RandIntBetween(r, 2, 9); i-- {
// Set up reward period parameters
start := rewardPeriodStart
end := start.Add(reward.Duration).UTC()
baseRewardAmount := reward.AvailableRewards.Amount.Quo(sdk.NewInt(100)) // base period reward is 1/100 total reward
// Earlier periods have larger rewards
amount := sdk.NewCoin(reward.Denom, baseRewardAmount.Mul(sdk.NewInt(int64(i))))
claimEnd := end.Add(reward.ClaimDuration)
claimTimeLock := reward.TimeLock
// Create reward period and append to array
rewardPeriod := types.NewRewardPeriod(reward.Denom, start, end, amount, claimEnd, claimTimeLock)
rewardPeriods = append(rewardPeriods, rewardPeriod)
// Update start time of next reward period
rewardPeriodStart = end
}
}
return rewardPeriods
}
// genClaimPeriods loads valid claim periods for an array of reward periods
func genClaimPeriods(rewardPeriods types.RewardPeriods) types.ClaimPeriods {
denomRewardPeriodsCount := make(map[string]uint64)
var claimPeriods types.ClaimPeriods
for _, rewardPeriod := range rewardPeriods {
// Increment reward period count for this denom (this is our claim period's ID)
denom := rewardPeriod.Denom
numbRewardPeriods := denomRewardPeriodsCount[denom] + 1
denomRewardPeriodsCount[denom] = numbRewardPeriods
// Set end and timelock from the associated reward period
end := rewardPeriod.ClaimEnd
claimTimeLock := rewardPeriod.ClaimTimeLock
// Create the new claim period for this reward period
claimPeriod := types.NewClaimPeriod(denom, numbRewardPeriods, end, claimTimeLock)
claimPeriods = append(claimPeriods, claimPeriod)
}
return claimPeriods
}
// genNextClaimPeriodIds returns an array of the most recent claim period IDs for each denom
func genNextClaimPeriodIds(cps types.ClaimPeriods) types.GenesisClaimPeriodIDs {
// Build a map of the most recent claim periods by denom
mostRecentClaimPeriodByDenom := make(map[string]uint64)
for _, cp := range cps {
if cp.ID > mostRecentClaimPeriodByDenom[cp.Denom] {
mostRecentClaimPeriodByDenom[cp.Denom] = cp.ID
}
}
// Write map contents to an array of GenesisClaimPeriodIDs
var claimPeriodIDs types.GenesisClaimPeriodIDs
for key, value := range mostRecentClaimPeriodByDenom {
claimPeriodID := types.GenesisClaimPeriodID{Denom: key, ID: value}
claimPeriodIDs = append(claimPeriodIDs, claimPeriodID)
}
return claimPeriodIDs
}
// In a list of accounts, replace the first account found with the same address. If not found, append the account.
func replaceOrAppendAccount(accounts []authexported.GenesisAccount, acc authexported.GenesisAccount) []authexported.GenesisAccount {
newAccounts := accounts
for i, a := range accounts {
if a.GetAddress().Equals(acc.GetAddress()) {
newAccounts[i] = acc
return newAccounts
}
}
return append(newAccounts, acc)
}

View File

@ -0,0 +1,146 @@
package simulation
import (
"fmt"
"math/rand"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/simapp/helpers"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
"github.com/cosmos/cosmos-sdk/x/simulation"
appparams "github.com/kava-labs/kava/app/params"
"github.com/kava-labs/kava/x/incentive/keeper"
"github.com/kava-labs/kava/x/incentive/types"
"github.com/kava-labs/kava/x/kavadist"
)
// Simulation operation weights constants
const (
OpWeightMsgClaimReward = "op_weight_msg_claim_reward"
)
// WeightedOperations returns all the operations from the module with their respective weights
func WeightedOperations(
appParams simulation.AppParams, cdc *codec.Codec, ak auth.AccountKeeper, sk types.SupplyKeeper, k keeper.Keeper,
) simulation.WeightedOperations {
var weightMsgClaimReward int
appParams.GetOrGenerate(cdc, OpWeightMsgClaimReward, &weightMsgClaimReward, nil,
func(_ *rand.Rand) {
weightMsgClaimReward = appparams.DefaultWeightMsgClaimReward
},
)
return simulation.WeightedOperations{
simulation.NewWeightedOperation(
weightMsgClaimReward,
SimulateMsgClaimReward(ak, sk, k),
),
}
}
// SimulateMsgClaimReward generates a MsgClaimReward
func SimulateMsgClaimReward(ak auth.AccountKeeper, sk types.SupplyKeeper, k keeper.Keeper) simulation.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string,
) (simulation.OperationMsg, []simulation.FutureOperation, error) {
// Load only account types that can claim rewards
var accounts []authexported.Account
validAccounts := make(map[string]bool)
for _, acc := range accs {
account := ak.GetAccount(ctx, acc.Address)
switch account.(type) {
case *vesting.PeriodicVestingAccount, *auth.BaseAccount: // Valid: BaseAccount, PeriodicVestingAccount
accounts = append(accounts, account)
validAccounts[account.GetAddress().String()] = true
break
default: // Invalid: ValidatorVestingAccount, DelayedVestingAccount, ContinuousVestingAccount
break
}
}
// Load open claims and shuffle them to randomize
openClaims := types.Claims{}
k.IterateClaims(ctx, func(claim types.Claim) bool {
openClaims = append(openClaims, claim)
return false
})
r.Shuffle(len(openClaims), func(i, j int) {
openClaims[i], openClaims[j] = openClaims[j], openClaims[i]
})
kavadistMacc := sk.GetModuleAccount(ctx, kavadist.KavaDistMacc)
kavadistBalance := kavadistMacc.SpendableCoins(ctx.BlockTime())
// Find address that has a claim of the same reward denom, then confirm it's distributable
claimer, claim, found := findValidAccountClaimPair(accs, openClaims, func(acc simulation.Account, claim types.Claim) bool {
if validAccounts[acc.Address.String()] { // Address must be valid type
if claim.Owner.Equals(acc.Address) { // Account must be claim owner
allClaims, found := k.GetClaimsByAddressAndDenom(ctx, claim.Owner, claim.Denom)
if found { // found should always be true
var rewards sdk.Coins
for _, individualClaim := range allClaims {
rewards = rewards.Add(individualClaim.Reward)
}
if rewards.AmountOf(claim.Reward.Denom).IsPositive() { // Can't distribute 0 coins
// Validate that kavadist module has enough coins to distribute rewards
if kavadistBalance.AmountOf(claim.Reward.Denom).GTE(rewards.AmountOf(claim.Reward.Denom)) {
return true
}
}
}
}
}
return false
})
if !found {
return simulation.NewOperationMsgBasic(types.ModuleName,
"no-operation (no accounts currently have fulfillable claims)", "", false, nil), nil, nil
}
claimerAcc := ak.GetAccount(ctx, claimer.Address)
if claimerAcc == nil {
return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("couldn't find account %s", claimer.Address)
}
msg := types.NewMsgClaimReward(claimer.Address, claim.Denom)
tx := helpers.GenTx(
[]sdk.Msg{msg},
sdk.NewCoins(),
helpers.DefaultGenTxGas,
chainID,
[]uint64{claimerAcc.GetAccountNumber()},
[]uint64{claimerAcc.GetSequence()},
claimer.PrivKey,
)
_, result, err := app.Deliver(tx)
if err != nil {
// to aid debugging, add the stack trace to the comment field of the returned opMsg
return simulation.NewOperationMsg(msg, false, fmt.Sprintf("%+v", err)), nil, err
}
// to aid debugging, add the result log to the comment field
return simulation.NewOperationMsg(msg, true, result.Log), nil, nil
}
}
// findValidAccountClaimPair finds an account and reward claim for which the callback func returns true
func findValidAccountClaimPair(accounts []simulation.Account, claims types.Claims,
cb func(simulation.Account, types.Claim) bool) (simulation.Account, types.Claim, bool) {
for _, claim := range claims {
for _, acc := range accounts {
if isValid := cb(acc, claim); isValid {
return acc, claim, true
}
}
}
return simulation.Account{}, types.Claim{}, false
}

View File

@ -0,0 +1,40 @@
package simulation
import (
"fmt"
"math/rand"
"github.com/cosmos/cosmos-sdk/x/simulation"
"github.com/kava-labs/kava/x/incentive/types"
)
const (
keyActive = "Active"
keyRewards = "Rewards"
)
// genActive generates active bool with 80% chance of true
func genActive(r *rand.Rand) bool {
threshold := 80
value := simulation.RandIntBetween(r, 1, 100)
if value > threshold {
return false
}
return true
}
// ParamChanges defines the parameters that can be modified by param change proposals
func ParamChanges(r *rand.Rand) []simulation.ParamChange {
return []simulation.ParamChange{
simulation.NewSimParamChange(types.ModuleName, keyActive,
func(r *rand.Rand) string {
return fmt.Sprintf("\"%t\"", genActive(r))
},
),
simulation.NewSimParamChange(types.ModuleName, keyRewards,
func(r *rand.Rand) string {
return fmt.Sprintf("\"%v\"", genRewards(r))
},
),
}
}

View File

@ -0,0 +1,14 @@
package types
import (
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
)
// GetTotalVestingPeriodLength returns the summed length of all vesting periods
func GetTotalVestingPeriodLength(periods vesting.Periods) int64 {
length := int64(0)
for _, period := range periods {
length += period.Length
}
return length
}

View File

@ -0,0 +1,51 @@
package types_test
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
"github.com/kava-labs/kava/x/incentive/types"
"github.com/stretchr/testify/suite"
)
type accountTest struct {
periods vesting.Periods
expectedVal int64
}
type AccountTestSuite struct {
suite.Suite
tests []accountTest
}
func (suite *AccountTestSuite) SetupTest() {
tests := []accountTest{
accountTest{
periods: vesting.Periods{
vesting.Period{
Length: int64(100),
Amount: sdk.Coins{},
},
vesting.Period{
Length: int64(200),
Amount: sdk.Coins{},
},
},
expectedVal: int64(300),
},
}
suite.tests = tests
}
func (suite *AccountTestSuite) TestGetTotalPeriodLength() {
for _, t := range suite.tests {
length := types.GetTotalVestingPeriodLength(t.periods)
suite.Equal(t.expectedVal, length)
}
}
func TestAccountTestSuite(t *testing.T) {
suite.Run(t, new(AccountTestSuite))
}

View File

@ -0,0 +1,22 @@
package types
import "github.com/cosmos/cosmos-sdk/codec"
// ModuleCdc generic sealed codec to be used throughout module
var ModuleCdc *codec.Codec
func init() {
cdc := codec.New()
RegisterCodec(cdc)
codec.RegisterCrypto(cdc)
ModuleCdc = cdc.Seal()
}
// RegisterCodec registers the necessary types for incentive module
func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(MsgClaimReward{}, "incentive/MsgClaimReward", nil)
cdc.RegisterConcrete(GenesisClaimPeriodID{}, "incentive/GenesisClaimPeriodID", nil)
cdc.RegisterConcrete(RewardPeriod{}, "incentive/RewardPeriod", nil)
cdc.RegisterConcrete(ClaimPeriod{}, "incentive/ClaimPeriod", nil)
cdc.RegisterConcrete(Claim{}, "incentive/Claim", nil)
}

View File

@ -0,0 +1,15 @@
package types
import (
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// DONTCOVER
var (
ErrClaimNotFound = sdkerrors.Register(ModuleName, 1, "no claim with input id found for owner and denom")
ErrClaimPeriodNotFound = sdkerrors.Register(ModuleName, 2, "no claim period found for id and denom")
ErrInvalidAccountType = sdkerrors.Register(ModuleName, 3, "account type not supported")
ErrNoClaimsFound = sdkerrors.Register(ModuleName, 4, "no claims with denom found for address")
ErrInsufficientModAccountBalance = sdkerrors.Register(ModuleName, 5, "module account has insufficient balance to pay claim")
)

View File

@ -0,0 +1,8 @@
package types
const (
EventTypeClaim = "claim_reward"
AttributeValueCategory = ModuleName
AttributeKeySender = "sender"
)

View File

@ -0,0 +1,27 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
)
// SupplyKeeper defines the expected supply keeper for module accounts
type SupplyKeeper interface {
GetModuleAccount(ctx sdk.Context, name string) supplyexported.ModuleAccountI
SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
}
// CdpKeeper defines the expected cdp keeper for interacting with cdps
type CdpKeeper interface {
IterateCdpsByDenom(ctx sdk.Context, denom string, cb func(cdp cdptypes.CDP) (stop bool))
GetTotalPrincipal(ctx sdk.Context, collateralDenom string, principalDenom string) (total sdk.Int)
}
// AccountKeeper defines the expected keeper interface for interacting with account
type AccountKeeper interface {
GetAccount(ctx sdk.Context, addr sdk.AccAddress) authexported.Account
SetAccount(ctx sdk.Context, acc authexported.Account)
}

View File

@ -0,0 +1,75 @@
package types
import (
"bytes"
"fmt"
"time"
)
// GenesisClaimPeriodID stores the next claim id and its corresponding denom
type GenesisClaimPeriodID struct {
Denom string `json:"denom" yaml:"denom"`
ID uint64 `json:"id" yaml:"id"`
}
// GenesisClaimPeriodIDs array of GenesisClaimPeriodID
type GenesisClaimPeriodIDs []GenesisClaimPeriodID
// GenesisState is the state that must be provided at genesis.
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"`
RewardPeriods RewardPeriods `json:"reward_periods" yaml:"reward_periods"`
ClaimPeriods ClaimPeriods `json:"claim_periods" yaml:"claim_periods"`
Claims Claims `json:"claims" yaml:"claims"`
NextClaimPeriodIDs GenesisClaimPeriodIDs `json:"next_claim_period_ids" yaml:"next_claim_period_ids"`
}
// NewGenesisState returns a new genesis state
func NewGenesisState(params Params, previousBlockTime time.Time, rp RewardPeriods, cp ClaimPeriods, c Claims, ids GenesisClaimPeriodIDs) GenesisState {
return GenesisState{
Params: params,
PreviousBlockTime: previousBlockTime,
RewardPeriods: rp,
ClaimPeriods: cp,
Claims: c,
NextClaimPeriodIDs: ids,
}
}
// DefaultGenesisState returns a default genesis state
func DefaultGenesisState() GenesisState {
return GenesisState{
Params: DefaultParams(),
PreviousBlockTime: DefaultPreviousBlockTime,
RewardPeriods: RewardPeriods{},
ClaimPeriods: ClaimPeriods{},
Claims: Claims{},
NextClaimPeriodIDs: GenesisClaimPeriodIDs{},
}
}
// Validate performs basic validation of genesis data returning an
// error for any failed validation criteria.
func (gs GenesisState) Validate() error {
if err := gs.Params.Validate(); err != nil {
return err
}
if gs.PreviousBlockTime.Equal(time.Time{}) {
return fmt.Errorf("previous block time 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{})
}

61
x/incentive/types/keys.go Normal file
View File

@ -0,0 +1,61 @@
package types
import (
"encoding/binary"
sdk "github.com/cosmos/cosmos-sdk/types"
)
const (
// ModuleName The name that will be used throughout the module
ModuleName = "incentive"
// StoreKey Top level store key where all module items will be stored
StoreKey = ModuleName
// RouterKey Top level router key
RouterKey = ModuleName
// DefaultParamspace default name for parameter store
DefaultParamspace = ModuleName
// QuerierRoute route used for abci queries
QuerierRoute = ModuleName
)
// Key Prefixes
var (
RewardPeriodKeyPrefix = []byte{0x01} // prefix for keys that store reward periods
ClaimPeriodKeyPrefix = []byte{0x02} // prefix for keys that store claim periods
ClaimKeyPrefix = []byte{0x03} // prefix for keys that store claims
NextClaimPeriodIDPrefix = []byte{0x04} // prefix for keys that store the next ID for claims periods
PreviousBlockTimeKey = []byte{0x05} // prefix for key that stores the previous blocktime
)
// Keys
// 0x00:Denom <> RewardPeriod the current active reward period (max 1 reward period per denom)
// 0x01:Denom:ID <> ClaimPeriod object for that ID, indexed by denom and ID
// 0x02:Denom:ID:Owner <> Claim object, indexed by Denom, ID and owner
// 0x03:Denom <> NextClaimPeriodIDPrefix the ID of the next claim period, indexed by denom
// BytesToUint64 returns uint64 format from a byte array
func BytesToUint64(bz []byte) uint64 {
return binary.BigEndian.Uint64(bz)
}
// GetClaimPeriodPrefix returns the key (denom + id) for a claim prefix
func GetClaimPeriodPrefix(denom string, id uint64) []byte {
return createKey([]byte(denom), sdk.Uint64ToBigEndian(id))
}
// GetClaimPrefix returns the key (denom + id + address) for a claim
func GetClaimPrefix(addr sdk.AccAddress, denom string, id uint64) []byte {
return createKey([]byte(denom), sdk.Uint64ToBigEndian(id), addr)
}
func createKey(bytes ...[]byte) (r []byte) {
for _, b := range bytes {
r = append(r, b...)
}
return
}

54
x/incentive/types/msg.go Normal file
View File

@ -0,0 +1,54 @@
package types
import (
"errors"
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// ensure Msg interface compliance at compile time
var _ sdk.Msg = &MsgClaimReward{}
// MsgClaimReward message type used to claim rewards
type MsgClaimReward struct {
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
Denom string `json:"denom" yaml:"denom"`
}
// NewMsgClaimReward returns a new MsgClaimReward.
func NewMsgClaimReward(sender sdk.AccAddress, denom string) MsgClaimReward {
return MsgClaimReward{
Sender: sender,
Denom: denom,
}
}
// Route return the message type used for routing the message.
func (msg MsgClaimReward) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgClaimReward) Type() string { return "claim_reward" }
// ValidateBasic does a simple validation check that doesn't require access to state.
func (msg MsgClaimReward) ValidateBasic() error {
if msg.Sender.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
if strings.TrimSpace(msg.Denom) == "" {
return errors.New("invalid (empty) denom")
}
return nil
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgClaimReward) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgClaimReward) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Sender}
}

View File

@ -0,0 +1,59 @@
package types_test
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/incentive/types"
"github.com/stretchr/testify/suite"
"github.com/tendermint/tendermint/crypto"
)
type msgTest struct {
from sdk.AccAddress
denom string
expectPass bool
}
type MsgTestSuite struct {
suite.Suite
tests []msgTest
}
func (suite *MsgTestSuite) SetupTest() {
tests := []msgTest{
msgTest{
from: sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))),
denom: "bnb",
expectPass: true,
},
msgTest{
from: sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))),
denom: "",
expectPass: false,
},
msgTest{
from: sdk.AccAddress{},
denom: "bnb",
expectPass: false,
},
}
suite.tests = tests
}
func (suite *MsgTestSuite) TestMsgValidation() {
for _, t := range suite.tests {
msg := types.NewMsgClaimReward(t.from, t.denom)
err := msg.ValidateBasic()
if t.expectPass {
suite.Require().NoError(err)
} else {
suite.Require().Error(err)
}
}
}
func TestMsgTestSuite(t *testing.T) {
suite.Run(t, new(MsgTestSuite))
}

165
x/incentive/types/params.go Normal file
View File

@ -0,0 +1,165 @@
package types
import (
"fmt"
"strings"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
kavadistTypes "github.com/kava-labs/kava/x/kavadist/types"
tmtime "github.com/tendermint/tendermint/types/time"
)
// Parameter keys and default values
var (
KeyActive = []byte("Active")
KeyRewards = []byte("Rewards")
DefaultActive = false
DefaultRewards = Rewards{}
DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(0, 0))
GovDenom = cdptypes.DefaultGovDenom
PrincipalDenom = "usdx"
IncentiveMacc = kavadistTypes.ModuleName
)
// Params governance parameters for the incentive module
type Params struct {
Active bool `json:"active" yaml:"active"` // top level governance switch to disable all rewards
Rewards Rewards `json:"rewards" yaml:"rewards"`
}
// NewParams returns a new params object
func NewParams(active bool, rewards Rewards) Params {
return Params{
Active: active,
Rewards: rewards,
}
}
// DefaultParams returns default params for kavadist module
func DefaultParams() Params {
return NewParams(DefaultActive, DefaultRewards)
}
// String implements fmt.Stringer
func (p Params) String() string {
return fmt.Sprintf(`Params:
Active: %t
Rewards: %s`, p.Active, p.Rewards)
}
// ParamKeyTable Key declaration for parameters
func ParamKeyTable() params.KeyTable {
return params.NewKeyTable().RegisterParamSet(&Params{})
}
// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs
func (p *Params) ParamSetPairs() params.ParamSetPairs {
return params.ParamSetPairs{
params.NewParamSetPair(KeyActive, &p.Active, validateActiveParam),
params.NewParamSetPair(KeyRewards, &p.Rewards, validateRewardsParam),
}
}
// Validate checks that the parameters have valid values.
func (p Params) Validate() error {
if err := validateActiveParam(p.Active); err != nil {
return err
}
if err := validateRewardsParam(p.Rewards); err != nil {
return err
}
return nil
}
func validateActiveParam(i interface{}) error {
_, ok := i.(bool)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
return nil
}
func validateRewardsParam(i interface{}) error {
rewards, ok := i.(Rewards)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
rewardDenoms := make(map[string]bool)
for _, reward := range rewards {
if strings.TrimSpace(reward.Denom) == "" {
return fmt.Errorf("cannot have empty reward denom: %s", reward)
}
if rewardDenoms[reward.Denom] {
return fmt.Errorf("cannot have duplicate reward denoms: %s", reward.Denom)
}
rewardDenoms[reward.Denom] = true
if !reward.AvailableRewards.IsValid() {
return fmt.Errorf("invalid reward coins %s for %s", reward.AvailableRewards, reward.Denom)
}
if !reward.AvailableRewards.IsPositive() {
return fmt.Errorf("reward amount must be positive, is %s for %s", reward.AvailableRewards, reward.Denom)
}
if int(reward.Duration.Seconds()) <= 0 {
return fmt.Errorf("reward duration must be positive, is %s for %s", reward.Duration.String(), reward.Denom)
}
if int(reward.TimeLock.Seconds()) < 0 {
return fmt.Errorf("reward timelock must be non-negative, is %s for %s", reward.TimeLock.String(), reward.Denom)
}
if int(reward.ClaimDuration.Seconds()) <= 0 {
return fmt.Errorf("claim duration must be positive, is %s for %s", reward.ClaimDuration.String(), reward.Denom)
}
}
return nil
}
// Reward stores the specified state for a single reward period.
type Reward struct {
Active bool `json:"active" yaml:"active"` // governance switch to disable a period
Denom string `json:"denom" yaml:"denom"` // the collateral denom rewards apply to, must be found in the cdp collaterals
AvailableRewards sdk.Coin `json:"available_rewards" yaml:"available_rewards"` // the total amount of coins distributed per period
Duration time.Duration `json:"duration" yaml:"duration"` // the duration of the period
TimeLock time.Duration `json:"time_lock" yaml:"time_lock"` // how long rewards for this period are timelocked
ClaimDuration time.Duration `json:"claim_duration" yaml:"claim_duration"` // how long users have after the period ends to claim their rewards
}
// NewReward returns a new Reward
func NewReward(active bool, denom string, reward sdk.Coin, duration time.Duration, timelock time.Duration, claimDuration time.Duration) Reward {
return Reward{
Active: active,
Denom: denom,
AvailableRewards: reward,
Duration: duration,
TimeLock: timelock,
ClaimDuration: claimDuration,
}
}
// String implements fmt.Stringer
func (r Reward) String() string {
return fmt.Sprintf(`Reward:
Active: %t,
Denom: %s,
Available Rewards: %s,
Duration: %s,
Time Lock: %s,
Claim Duration: %s`,
r.Active, r.Denom, r.AvailableRewards, r.Duration, r.TimeLock, r.ClaimDuration)
}
// Rewards array of Reward
type Rewards []Reward
// String implements fmt.Stringer
func (rs Rewards) String() string {
out := "Rewards\n"
for _, r := range rs {
out += fmt.Sprintf("%s\n", r)
}
return out
}

View File

@ -0,0 +1,219 @@
package types_test
import (
"strings"
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/incentive/types"
"github.com/stretchr/testify/suite"
)
type paramTest struct {
name string
params types.Params
errResult errResult
}
type errResult struct {
expectPass bool
contains string
}
type ParamTestSuite struct {
suite.Suite
tests []paramTest
}
func (suite *ParamTestSuite) SetupTest() {
suite.tests = []paramTest{
paramTest{
name: "valid - active",
params: types.Params{
Active: true,
Rewards: types.Rewards{
types.Reward{
Active: true,
Denom: "bnb",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
Duration: time.Hour * 24 * 7,
TimeLock: time.Hour * 8766,
ClaimDuration: time.Hour * 24 * 14,
},
},
},
errResult: errResult{
expectPass: true,
contains: "",
},
},
paramTest{
name: "valid - inactive",
params: types.Params{
Active: false,
Rewards: types.Rewards{
types.Reward{
Active: true,
Denom: "bnb",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
Duration: time.Hour * 24 * 7,
TimeLock: time.Hour * 8766,
ClaimDuration: time.Hour * 24 * 14,
},
},
},
errResult: errResult{
expectPass: true,
contains: "",
},
},
paramTest{
name: "duplicate reward",
params: types.Params{
Active: true,
Rewards: types.Rewards{
types.Reward{
Active: true,
Denom: "bnb",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
Duration: time.Hour * 24 * 7,
TimeLock: time.Hour * 8766,
ClaimDuration: time.Hour * 24 * 14,
},
types.Reward{
Active: true,
Denom: "bnb",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
Duration: time.Hour * 24 * 7,
TimeLock: time.Hour * 8766,
ClaimDuration: time.Hour * 24 * 14,
},
},
},
errResult: errResult{
expectPass: false,
contains: "cannot have duplicate reward denoms",
},
},
paramTest{
name: "negative reward duration",
params: types.Params{
Active: true,
Rewards: types.Rewards{
types.Reward{
Active: true,
Denom: "bnb",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
Duration: time.Hour * -24 * 7,
TimeLock: time.Hour * 8766,
ClaimDuration: time.Hour * 24 * 14,
},
},
},
errResult: errResult{
expectPass: false,
contains: "reward duration must be positive",
},
},
paramTest{
name: "negative time lock",
params: types.Params{
Active: true,
Rewards: types.Rewards{
types.Reward{
Active: true,
Denom: "bnb",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
Duration: time.Hour * 24 * 7,
TimeLock: time.Hour * -8766,
ClaimDuration: time.Hour * 24 * 14,
},
},
},
errResult: errResult{
expectPass: false,
contains: "reward timelock must be non-negative",
},
},
paramTest{
name: "zero claim duration",
params: types.Params{
Active: true,
Rewards: types.Rewards{
types.Reward{
Active: true,
Denom: "bnb",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
Duration: time.Hour * 24 * 7,
TimeLock: time.Hour * 8766,
ClaimDuration: time.Hour * 0,
},
},
},
errResult: errResult{
expectPass: false,
contains: "claim duration must be positive",
},
},
paramTest{
name: "zero reward",
params: types.Params{
Active: true,
Rewards: types.Rewards{
types.Reward{
Active: true,
Denom: "bnb",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(0)),
Duration: time.Hour * 24 * 7,
TimeLock: time.Hour * 8766,
ClaimDuration: time.Hour * 24 * 14,
},
},
},
errResult: errResult{
expectPass: false,
contains: "reward amount must be positive",
},
},
paramTest{
name: "empty reward denom",
params: types.Params{
Active: true,
Rewards: types.Rewards{
types.Reward{
Active: true,
Denom: "",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(0)),
Duration: time.Hour * 24 * 7,
TimeLock: time.Hour * 8766,
ClaimDuration: time.Hour * 24 * 14,
},
},
},
errResult: errResult{
expectPass: false,
contains: "cannot have empty reward denom",
},
},
}
}
func (suite *ParamTestSuite) TestParamValidation() {
for _, t := range suite.tests {
suite.Run(t.name, func() {
err := t.params.Validate()
if t.errResult.expectPass {
suite.Require().NoError(err)
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), t.errResult.contains))
}
})
}
}
func TestParamTestSuite(t *testing.T) {
suite.Run(t, new(ParamTestSuite))
}

View File

@ -0,0 +1,11 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
)
// NewPeriod returns a new vesting period
func NewPeriod(amount sdk.Coins, length int64) vesting.Period {
return vesting.Period{Amount: amount, Length: length}
}

View File

@ -0,0 +1,35 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
)
// Querier routes for the incentive module
const (
QueryGetClaims = "claims"
RestClaimOwner = "owner"
RestClaimDenom = "denom"
QueryGetParams = "parameters"
)
// QueryClaimsParams params for query /incentive/claims
type QueryClaimsParams struct {
Owner sdk.AccAddress
Denom string
}
// NewQueryClaimsParams returns QueryClaimsParams
func NewQueryClaimsParams(owner sdk.AccAddress, denom string) QueryClaimsParams {
return QueryClaimsParams{
Owner: owner,
Denom: denom,
}
}
// PostClaimReq defines the properties of claim transaction's request body.
type PostClaimReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
Denom string `json:"denom" yaml:"denom"`
}

View File

@ -0,0 +1,97 @@
package types
import (
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// RewardPeriod stores the state of an ongoing reward
type RewardPeriod struct {
Denom string `json:"denom" yaml:"denom"`
Start time.Time `json:"start" yaml:"start"`
End time.Time `json:"end" yaml:"end"`
Reward sdk.Coin `json:"reward" yaml:"reward"` // per second reward payouts
ClaimEnd time.Time `json:"claim_end" yaml:"claim_end"`
ClaimTimeLock time.Duration `json:"claim_time_lock" yaml:"claim_time_lock"` // the amount of time rewards are timelocked once they are sent to users
}
// String implements fmt.Stringer
func (rp RewardPeriod) String() string {
return fmt.Sprintf(`Reward Period:
Denom: %s,
Start: %s,
End: %s,
Reward: %s,
Claim End: %s,
Claim Time Lock: %s`,
rp.Denom, rp.Start, rp.End, rp.Reward, rp.ClaimEnd, rp.ClaimTimeLock)
}
// NewRewardPeriod returns a new RewardPeriod
func NewRewardPeriod(denom string, start time.Time, end time.Time, reward sdk.Coin, claimEnd time.Time, claimTimeLock time.Duration) RewardPeriod {
return RewardPeriod{
Denom: denom,
Start: start,
End: end,
Reward: reward,
ClaimEnd: claimEnd,
ClaimTimeLock: claimTimeLock,
}
}
// RewardPeriods array of RewardPeriod
type RewardPeriods []RewardPeriod
// ClaimPeriod stores the state of an ongoing claim period
type ClaimPeriod struct {
Denom string `json:"denom" yaml:"denom"`
ID uint64 `json:"id" yaml:"id"`
End time.Time `json:"end" yaml:"end"`
TimeLock time.Duration `json:"time_lock" yaml:"time_lock"`
}
// NewClaimPeriod returns a new ClaimPeriod
func NewClaimPeriod(denom string, id uint64, end time.Time, timeLock time.Duration) ClaimPeriod {
return ClaimPeriod{
Denom: denom,
ID: id,
End: end,
TimeLock: timeLock,
}
}
// ClaimPeriods array of ClaimPeriod
type ClaimPeriods []ClaimPeriod
// Claim stores the rewards that can be claimed by owner
type Claim struct {
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
Reward sdk.Coin `json:"reward" yaml:"reward"`
Denom string `json:"denom" yaml:"denom"`
ClaimPeriodID uint64 `json:"claim_period_id" yaml:"claim_period_id"`
}
// NewClaim returns a new Claim
func NewClaim(owner sdk.AccAddress, reward sdk.Coin, denom string, claimPeriodID uint64) Claim {
return Claim{
Owner: owner,
Reward: reward,
Denom: denom,
ClaimPeriodID: claimPeriodID,
}
}
// String implements fmt.Stringer
func (c Claim) String() string {
return fmt.Sprintf(`Claim:
Owner: %s,
Denom: %s,
Reward: %s,
Claim Period ID: %d,`,
c.Owner, c.Denom, c.Reward, c.ClaimPeriodID)
}
// Claims array of Claim
type Claims []Claim

View File

@ -14,10 +14,11 @@ import (
)
// SecondsPerYear is the number of seconds in a year
const SecondsPerYear = 31536000
// BaseAprPadding prevents the calculated SPR inflation rate from being 0.0
const BaseAprPadding = "0.000000000100000000"
const (
SecondsPerYear = 31536000
// BaseAprPadding sets the minimum inflation to the calculated SPR inflation rate from being 0.0
BaseAprPadding = "0.000000003022265980"
)
// RandomizedGenState generates a random GenesisState for kavadist module
func RandomizedGenState(simState *module.SimulationState) {
@ -46,8 +47,8 @@ func genRandomPeriods(r *rand.Rand, timestamp time.Time) types.Periods {
numPeriods := simulation.RandIntBetween(r, 1, 10)
periodStart := timestamp
for i := 0; i < numPeriods; i++ {
// set periods to be between 2 weeks and 2 years
durationMultiplier := simulation.RandIntBetween(r, 14, 104)
// set periods to be between 1-3 days
durationMultiplier := simulation.RandIntBetween(r, 1, 3)
duration := time.Duration(int64(24*durationMultiplier)) * time.Hour
periodEnd := periodStart.Add(duration)
inflation := genRandomInflation(r)
@ -59,13 +60,14 @@ func genRandomPeriods(r *rand.Rand, timestamp time.Time) types.Periods {
}
func genRandomInflation(r *rand.Rand) sdk.Dec {
// If sim.RandomDecAmount returns 0 (happens frequently by design), add BaseAprPadding
// If sim.RandomDecAmount is less than base apr padding, add base apr padding
aprPadding, _ := sdk.NewDecFromStr(BaseAprPadding)
extraAprInflation := simulation.RandomDecAmount(r, sdk.MustNewDecFromStr("0.25"))
for extraAprInflation.Equal(sdk.ZeroDec()) {
extraAprInflation = extraAprInflation.Add(sdk.MustNewDecFromStr(BaseAprPadding))
for extraAprInflation.LT(aprPadding) {
extraAprInflation = extraAprInflation.Add(aprPadding)
}
aprInflation := sdk.OneDec().Add(extraAprInflation)
// convert APR inflation to SPR (inflation per second)
inflationSpr, err := approxRoot(aprInflation, uint64(SecondsPerYear))
if err != nil {

View File

@ -6,7 +6,7 @@ import (
"time"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/x/auth/exported"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/validator-vesting/types"
"github.com/tendermint/tendermint/libs/kv"
)
@ -15,9 +15,9 @@ import (
func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
switch {
case bytes.Equal(kvA.Key[:1], types.ValidatorVestingAccountPrefix):
var accA, accB exported.Account
cdc.MustUnmarshalBinaryBare(kvA.Value, &accA)
cdc.MustUnmarshalBinaryBare(kvB.Value, &accB)
var accA, accB sdk.AccAddress
accA = sdk.AccAddress(kvA.Key[1:])
accB = sdk.AccAddress(kvB.Key[1:])
return fmt.Sprintf("%v\n%v", accA, accB)
case bytes.Equal(kvA.Key, types.BlocktimeKey):
var btA, btB time.Time

View File

@ -5,14 +5,18 @@ import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/libs/kv"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/kava-labs/kava/x/validator-vesting/types"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto/ed25519"
"github.com/tendermint/tendermint/libs/kv"
)
var (
pk1 = ed25519.GenPrivKey().PubKey()
addr1 = sdk.AccAddress(pk1.Address())
)
func makeTestCodec() (cdc *codec.Codec) {
@ -28,11 +32,10 @@ func makeTestCodec() (cdc *codec.Codec) {
func TestDecodeDistributionStore(t *testing.T) {
cdc := makeTestCodec()
acc := types.ValidatorVestingAccount{SigningThreshold: 1}
now := time.Now().UTC()
kvPairs := kv.Pairs{
kv.Pair{Key: types.ValidatorVestingAccountPrefix, Value: cdc.MustMarshalBinaryBare(acc)},
kv.Pair{Key: append(types.ValidatorVestingAccountPrefix, addr1.Bytes()...), Value: []byte{0}},
kv.Pair{Key: types.BlocktimeKey, Value: cdc.MustMarshalBinaryLengthPrefixed(now)},
kv.Pair{Key: []byte{0x99}, Value: []byte{0x99}},
}
@ -41,7 +44,7 @@ func TestDecodeDistributionStore(t *testing.T) {
name string
expectedLog string
}{
{"ValidatorVestingAccount", fmt.Sprintf("%v\n%v", acc, acc)},
{"ValidatorVestingAccount", fmt.Sprintf("%v\n%v", addr1, addr1)},
{"BlockTime", fmt.Sprintf("%s\n%s", now, now)},
{"other", ""},
}

View File

@ -73,6 +73,8 @@ func RandomizedGenState(simState *module.SimulationState) {
}
newAuthGenesis := authtypes.NewGenesisState(authGenState.Params, newGenesisAccs)
simState.GenState[authtypes.ModuleName] = simState.Cdc.MustMarshalJSON(newAuthGenesis)
vestGenState := types.DefaultGenesisState()
simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(vestGenState)
}
func getRandomValidatorConsAddr(simState *module.SimulationState, rint int) sdk.ConsAddress {