From 6ea518960aa1b6a99b4b5f71f219fd4cdfe60889 Mon Sep 17 00:00:00 2001 From: Nick DeLuca Date: Tue, 26 Mar 2024 13:06:26 -0700 Subject: [PATCH] Optimize CDP Begin Blocker (#1822) * optimize cdp begin blocker by removing unnecessary checks, reusing data and prefix stores in loops, and reducing number of repeated calculations * fix panic for new cdp types if both previous accural time and global interest factor are not set * do not touch global interest factor if no CDP's exist; revert to panic if global interest factor is not found since this is an unreachable state by normal keeper operation -- it can only be reached if store is modified outside of public interface and normal operation --- x/cdp/abci.go | 2 +- x/cdp/abci_test.go | 86 +++++++++++++++++++++++++- x/cdp/keeper/interest.go | 110 ++++++++++++++++++++++++++++++++-- x/cdp/keeper/interest_test.go | 14 ++++- 4 files changed, 203 insertions(+), 9 deletions(-) diff --git a/x/cdp/abci.go b/x/cdp/abci.go index b44b276b..9967bd51 100644 --- a/x/cdp/abci.go +++ b/x/cdp/abci.go @@ -47,7 +47,7 @@ func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k keeper.Keeper) ctx.Logger().Debug(fmt.Sprintf("running x/cdp SynchronizeInterestForRiskyCDPs and LiquidateCdps for %s", cp.Type)) - err = k.SynchronizeInterestForRiskyCDPs(ctx, cp.CheckCollateralizationIndexCount, sdk.MaxSortableDec, cp.Type) + err = k.SynchronizeInterestForRiskyCDPs(ctx, sdk.MaxSortableDec, cp) if err != nil { panic(err) } diff --git a/x/cdp/abci_test.go b/x/cdp/abci_test.go index 0640aa06..eaffaa96 100644 --- a/x/cdp/abci_test.go +++ b/x/cdp/abci_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/suite" + sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/simulation" @@ -19,6 +20,7 @@ import ( "github.com/kava-labs/kava/x/cdp" "github.com/kava-labs/kava/x/cdp/keeper" "github.com/kava-labs/kava/x/cdp/types" + pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types" ) type ModuleTestSuite struct { @@ -43,7 +45,7 @@ func (suite *ModuleTestSuite) SetupTest() { ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: tmtime.Now()}) tracker := liquidationTracker{} - coins := cs(c("btc", 100000000), c("xrp", 10000000000)) + coins := cs(c("btc", 100000000), c("xrp", 10000000000), c("erc20/usdc", 10000000000)) _, addrs := app.GeneratePrivKeyAddressPairs(100) authGS := app.NewFundedGenStateWithSameCoins(tApp.AppCodec(), coins, addrs) tApp.InitializeFromGenesisStates( @@ -65,7 +67,7 @@ func (suite *ModuleTestSuite) createCdps() { cdps := make(types.CDPs, 100) tracker := liquidationTracker{} - coins := cs(c("btc", 100000000), c("xrp", 10000000000)) + coins := cs(c("btc", 100000000), c("xrp", 10000000000), c("erc20/usdc", 10000000000)) _, addrs := app.GeneratePrivKeyAddressPairs(100) authGS := app.NewFundedGenStateWithSameCoins(tApp.AppCodec(), coins, addrs) @@ -124,6 +126,86 @@ func (suite *ModuleTestSuite) setPrice(price sdk.Dec, market string) { suite.Equal(price, pp.Price) } +func (suite *ModuleTestSuite) TestBeginBlockNewCdpTypeSetsGlobalInterest() { + suite.createCdps() + + // add a new collateral that does not have previous accumulation time or global interest factor set + params := suite.keeper.GetParams(suite.ctx) + usdcCollateral := types.CollateralParam{ + Denom: "erc20/usdc", + Type: "erc20-usdc", + LiquidationRatio: sdk.MustNewDecFromStr("1.01"), + DebtLimit: sdk.NewInt64Coin("usdx", 500000000000), + StabilityFee: sdk.OneDec(), + AuctionSize: sdkmath.NewIntFromUint64(10000000000), + LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), + CheckCollateralizationIndexCount: sdkmath.NewInt(10), + KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"), + SpotMarketID: "usdc:usd", + LiquidationMarketID: "usdc:usd", + ConversionFactor: sdkmath.NewInt(6), + } + usdtCollateral := types.CollateralParam{ + Denom: "erc20/usdt", + Type: "erc20-usdt", + LiquidationRatio: sdk.MustNewDecFromStr("1.01"), + DebtLimit: sdk.NewInt64Coin("usdx", 500000000000), + StabilityFee: sdk.OneDec(), + AuctionSize: sdkmath.NewIntFromUint64(10000000000), + LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), + CheckCollateralizationIndexCount: sdkmath.NewInt(10), + KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"), + SpotMarketID: "usdt:usd", + LiquidationMarketID: "usdt:usd", + ConversionFactor: sdkmath.NewInt(18), + } + newCollaterals := []types.CollateralParam{usdcCollateral, usdtCollateral} + params.CollateralParams = append(params.CollateralParams, newCollaterals...) + suite.keeper.SetParams(suite.ctx, params) + + // setup market for cdp collateral + priceFeedKeeper := suite.app.GetPriceFeedKeeper() + priceParams := priceFeedKeeper.GetParams(suite.ctx) + newMarkets := []pricefeedtypes.Market{ + {MarketID: "usdc:usd", BaseAsset: "usdc", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + {MarketID: "usdt:usd", BaseAsset: "usdt", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + } + priceParams.Markets = append(priceParams.Markets, newMarkets...) + priceFeedKeeper.SetParams(suite.ctx, priceParams) + suite.setPrice(d("1"), "usdc:usd") + suite.keeper.UpdatePricefeedStatus(suite.ctx, usdcCollateral.SpotMarketID) + suite.setPrice(d("1"), "usdt:usd") + suite.keeper.UpdatePricefeedStatus(suite.ctx, usdtCollateral.SpotMarketID) + + // create a CDP for USDC, no CDPS for USDT + err := suite.keeper.AddCdp(suite.ctx, suite.addrs[0], c(usdcCollateral.Denom, 100000000), c("usdx", 10000000), usdcCollateral.Type) + suite.Require().NoError(err) + + // ensure begin block does not panic due to no accumulation time or no global interest factor + suite.Require().NotPanics(func() { + cdp.BeginBlocker(suite.ctx, abci.RequestBeginBlock{Header: suite.ctx.BlockHeader()}, suite.keeper) + }, "expected begin blocker not to panic") + + // set by accumulate interest (or add cdp above) + // usdc has accural time set + previousAccrualTime, found := suite.keeper.GetPreviousAccrualTime(suite.ctx, usdcCollateral.Type) + suite.Require().True(found, "expected previous accrual time for new market to be set") + suite.Equal(suite.ctx.BlockTime(), previousAccrualTime, "expected previous accrual time to equal block time") + // usdt has accural time set + previousAccrualTime, found = suite.keeper.GetPreviousAccrualTime(suite.ctx, usdtCollateral.Type) + suite.Require().True(found, "expected previous accrual time for new market to be set") + suite.Equal(suite.ctx.BlockTime(), previousAccrualTime, "expected previous accrual time to equal block time") + + // set for USDC by AddCdp + globalInterestFactor, found := suite.keeper.GetInterestFactor(suite.ctx, usdcCollateral.Type) + suite.Require().True(found, "expected global interest factor for new collateral to be set") + suite.Equal(sdk.OneDec(), globalInterestFactor, "expected global interest factor to equal 1") + // not set for USDT since it has no cdps + globalInterestFactor, found = suite.keeper.GetInterestFactor(suite.ctx, usdtCollateral.Type) + suite.Require().False(found, "expected global interest factor for new collateral to not be set") + suite.Equal(sdk.ZeroDec(), globalInterestFactor, "expected global interest factor to equal 0") +} + func (suite *ModuleTestSuite) TestBeginBlock() { // test setup, creating // 50 xrp cdps each with diff --git a/x/cdp/keeper/interest.go b/x/cdp/keeper/interest.go index 610686f5..36a566ef 100644 --- a/x/cdp/keeper/interest.go +++ b/x/cdp/keeper/interest.go @@ -5,6 +5,7 @@ import ( "math" sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/kava-labs/kava/x/cdp/types" @@ -161,11 +162,110 @@ func (k Keeper) CalculateNewInterest(ctx sdk.Context, cdp types.CDP) sdk.Coin { } // SynchronizeInterestForRiskyCDPs synchronizes the interest for the slice of cdps with the lowest collateral:debt ratio -func (k Keeper) SynchronizeInterestForRiskyCDPs(ctx sdk.Context, slice sdkmath.Int, targetRatio sdk.Dec, collateralType string) error { - cdps := k.GetSliceOfCDPsByRatioAndType(ctx, slice, targetRatio, collateralType) - for _, cdp := range cdps { - k.hooks.BeforeCDPModified(ctx, cdp) - k.SynchronizeInterest(ctx, cdp) +func (k Keeper) SynchronizeInterestForRiskyCDPs(ctx sdk.Context, targetRatio sdk.Dec, cp types.CollateralParam) error { + debtParam := k.GetParams(ctx).DebtParam + + cdpStore := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix) + collateralRatioStore := prefix.NewStore(ctx.KVStore(k.key), types.CollateralRatioIndexPrefix) + + cdpIDs := make([]uint64, 0, cp.CheckCollateralizationIndexCount.Int64()) + + iterator := collateralRatioStore.Iterator(types.CollateralRatioIterKey(cp.Type, sdk.ZeroDec()), types.CollateralRatioIterKey(cp.Type, targetRatio)) + for ; iterator.Valid(); iterator.Next() { + _, id, _ := types.SplitCollateralRatioKey(iterator.Key()) + cdpIDs = append(cdpIDs, id) + if int64(len(cdpIDs)) >= cp.CheckCollateralizationIndexCount.Int64() { + break + } } + iterator.Close() + + globalInterestFactor, found := k.GetInterestFactor(ctx, cp.Type) + if !found && len(cdpIDs) > 0 { + panic(fmt.Sprintf("global interest factor not found for type %s", cp.Type)) + } + prevAccrualTime, found := k.GetPreviousAccrualTime(ctx, cp.Type) + if !found { + panic(fmt.Sprintf("previous accrual time not found for type %s", cp.Type)) + } + + for _, cdpID := range cdpIDs { + // + // GET CDP + // + bz := cdpStore.Get(types.CdpKey(cp.Type, cdpID)) + if bz == nil { + panic(fmt.Sprintf("cdp %d does not exist", cdpID)) + } + var cdp types.CDP + k.cdc.MustUnmarshal(bz, &cdp) + + if debtParam.Denom != cdp.GetTotalPrincipal().Denom { + panic(fmt.Sprintf("unkown debt param %s", cdp.GetTotalPrincipal().Denom)) + } + + // + // HOOK + // + k.hooks.BeforeCDPModified(ctx, cdp) + + // + // CALC INTEREST + // + accumulatedInterest := sdk.ZeroInt() + cdpInterestFactor := globalInterestFactor.Quo(cdp.InterestFactor) + if !cdpInterestFactor.Equal(sdk.OneDec()) { + accumulatedInterest = sdk.NewDecFromInt(cdp.GetTotalPrincipal().Amount).Mul(cdpInterestFactor).RoundInt().Sub(cdp.GetTotalPrincipal().Amount) + } + + if accumulatedInterest.IsZero() { + // accumulated interest is zero if apy is zero or are if the total fees for all cdps round to zero + if cdp.FeesUpdated.Equal(prevAccrualTime) { + // if all fees are rounding to zero, don't update FeesUpdated + continue + } + // if apy is zero, we need to update FeesUpdated + cdp.FeesUpdated = prevAccrualTime + bz = k.cdc.MustMarshal(&cdp) + cdpStore.Set(types.CdpKey(cdp.Type, cdp.ID), bz) + } + + // + // GET OLD RATIO + // + previousCollateralRatio := calculateCollateralRatio(debtParam, cp, cdp) + + // + // UPDATE CDP + // + cdp.AccumulatedFees = cdp.AccumulatedFees.Add(sdk.NewCoin(cdp.AccumulatedFees.Denom, accumulatedInterest)) + cdp.FeesUpdated = prevAccrualTime + cdp.InterestFactor = globalInterestFactor + + // + // CALC NEW RATIO + // + updatedCollateralRatio := calculateCollateralRatio(debtParam, cp, cdp) + + // + // UPDATE STORE + // + collateralRatioStore.Delete(types.CollateralRatioKey(cdp.Type, cdp.ID, previousCollateralRatio)) + bz = k.cdc.MustMarshal(&cdp) + cdpStore.Set(types.CdpKey(cdp.Type, cdp.ID), bz) + collateralRatioStore.Set(types.CollateralRatioKey(cdp.Type, cdp.ID, updatedCollateralRatio), types.GetCdpIDBytes(cdp.ID)) + } + return nil } + +func calculateCollateralRatio(debtParam types.DebtParam, collateralParam types.CollateralParam, cdp types.CDP) sdk.Dec { + debtTotal := sdk.NewDecFromInt(cdp.GetTotalPrincipal().Amount).Mul(sdk.NewDecFromIntWithPrec(sdk.OneInt(), debtParam.ConversionFactor.Int64())) + + if debtTotal.IsZero() || debtTotal.GTE(types.MaxSortableDec) { + return types.MaxSortableDec.Sub(sdk.SmallestDec()) + } else { + collateralBaseUnits := sdk.NewDecFromInt(cdp.Collateral.Amount).Mul(sdk.NewDecFromIntWithPrec(sdk.OneInt(), collateralParam.ConversionFactor.Int64())) + return collateralBaseUnits.Quo(debtTotal) + } +} diff --git a/x/cdp/keeper/interest_test.go b/x/cdp/keeper/interest_test.go index d3881fc2..622283f4 100644 --- a/x/cdp/keeper/interest_test.go +++ b/x/cdp/keeper/interest_test.go @@ -713,7 +713,19 @@ func (suite *InterestTestSuite) TestSyncInterestForRiskyCDPs() { err = suite.keeper.AccumulateInterest(suite.ctx, tc.args.ctype) suite.Require().NoError(err) - err = suite.keeper.SynchronizeInterestForRiskyCDPs(suite.ctx, i(int64(tc.args.slice)), sdk.MaxSortableDec, tc.args.ctype) + params := suite.keeper.GetParams(suite.ctx) + var ctype types.CollateralParam + + for _, cp := range params.CollateralParams { + if cp.Type == tc.args.ctype { + ctype = cp + + cp.CheckCollateralizationIndexCount = sdk.NewInt(int64(tc.args.slice)) + break + } + } + + err = suite.keeper.SynchronizeInterestForRiskyCDPs(suite.ctx, sdk.MaxSortableDec, ctype) suite.Require().NoError(err) cdpsUpdatedCount := 0