diff --git a/app/app.go b/app/app.go index 4346fabec..b0e0a36d0 100644 --- a/app/app.go +++ b/app/app.go @@ -1064,6 +1064,7 @@ func NewElysApp( app.StablestakeKeeper = *app.StablestakeKeeper.SetHooks(stablestakekeeper.NewMultiStableStakeHooks( app.MasterchefKeeper.StableStakeHooks(), app.TierKeeper.StableStakeHooks(), + app.LeveragelpKeeper.StableStakeHooks(), )) stablestakeModule := stablestake.NewAppModule(appCodec, app.StablestakeKeeper, app.AccountKeeper, app.BankKeeper) diff --git a/x/leveragelp/genesis.go b/x/leveragelp/genesis.go index 7ff649997..5c1a2cdae 100644 --- a/x/leveragelp/genesis.go +++ b/x/leveragelp/genesis.go @@ -15,7 +15,7 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) // Set all the pool for _, elem := range genState.PositionList { - k.SetPosition(ctx, &elem) + k.SetPosition(ctx, &elem, sdk.NewInt(0)) } // Set genesis Position count diff --git a/x/leveragelp/keeper/add_collateral.go b/x/leveragelp/keeper/add_collateral.go index 0a550af4c..c056ddfb4 100644 --- a/x/leveragelp/keeper/add_collateral.go +++ b/x/leveragelp/keeper/add_collateral.go @@ -25,6 +25,7 @@ func (k Keeper) ProcessAddCollateral(ctx sdk.Context, address string, id uint64, return errorsmod.Wrap(types.ErrPositionDisabled, fmt.Sprintf("poolId: %d", position.AmmPoolId)) } + oldDebt := k.stableKeeper.GetDebt(ctx, position.GetPositionAddress()) // Check if collateral is not more than borrowed debtBefore := k.stableKeeper.UpdateInterestStackedByAddress(ctx, position.GetPositionAddress()) maxAllowedCollateral := debtBefore.Borrowed.Add(debtBefore.InterestStacked).Sub(debtBefore.InterestPaid) @@ -65,7 +66,7 @@ func (k Keeper) ProcessAddCollateral(ctx sdk.Context, address string, id uint64, position.Liabilities = debt.Borrowed position.Collateral = position.Collateral.Add(sdk.NewCoin(position.Collateral.Denom, collateral)) - k.SetPosition(ctx, &position) + k.SetPosition(ctx, &position, oldDebt.Borrowed.Add(oldDebt.InterestStacked).Sub(oldDebt.InterestPaid)) if k.hooks != nil { k.hooks.AfterLeveragelpPositionModified(ctx, ammPool, pool) diff --git a/x/leveragelp/keeper/begin_blocker.go b/x/leveragelp/keeper/begin_blocker.go index 794531ae6..6a78848a1 100644 --- a/x/leveragelp/keeper/begin_blocker.go +++ b/x/leveragelp/keeper/begin_blocker.go @@ -26,18 +26,11 @@ func (k Keeper) BeginBlocker(ctx sdk.Context) { continue } if k.IsPoolEnabled(ctx, pool.AmmPoolId) { - positions, _, _ := k.GetPositionsForPool(ctx, pool.AmmPoolId, nil) - for _, position := range positions { - k.LiquidatePositionIfUnhealthy(ctx, position, pool, ammPool) - } - // Liquidate positions liquidation health threshold // Design - // - `Health = PositionValue / Debt`, PositionValue is based on LpToken price change - // - Debt growth speed is relying on debt.Borrowed. - // - Things are sorted by `LeveragedLpAmount / debt.Borrowed` per pool to liquidate efficiently - - // TODO: should consider InterestStacked-InterestPaid amount + // - `Health = PositionValue / liability`, PositionValue is based on LpToken price change + // - Debt growth speed is relying on liability. + // - Things are sorted by `LeveragedLpAmount / liability` per pool to liquidate efficiently k.IteratePoolPosIdsLiquidationSorted(ctx, pool.AmmPoolId, func(posId types.AddressId) bool { position, err := k.GetPosition(ctx, posId.Address, posId.Id) if err != nil { @@ -80,8 +73,9 @@ func (k Keeper) LiquidatePositionIfUnhealthy(ctx sdk.Context, position *types.Po ctx.Logger().Error(errors.Wrap(err, fmt.Sprintf("error updating position health: %s", position.String())).Error()) return false, true } + debt := k.stableKeeper.GetDebt(ctx, position.GetPositionAddress()) position.PositionHealth = h - k.SetPosition(ctx, position) + k.SetPosition(ctx, position, debt.Borrowed.Add(debt.InterestStacked).Sub(debt.InterestPaid)) params := k.GetParams(ctx) isHealthy = position.PositionHealth.GT(params.SafetyFactor) @@ -119,8 +113,9 @@ func (k Keeper) ClosePositionIfUnderStopLossPrice(ctx sdk.Context, position *typ ctx.Logger().Error(errors.Wrap(err, fmt.Sprintf("error updating position health: %s", position.String())).Error()) return false, true } + debt := k.stableKeeper.GetDebt(ctx, position.GetPositionAddress()) position.PositionHealth = h - k.SetPosition(ctx, position) + k.SetPosition(ctx, position, debt.Borrowed.Add(debt.InterestStacked).Sub(debt.InterestPaid)) lpTokenPrice, err := ammPool.LpTokenPrice(ctx, k.oracleKeeper) if err != nil { diff --git a/x/leveragelp/keeper/begin_blocker_test.go b/x/leveragelp/keeper/begin_blocker_test.go index c951e88e6..52e863816 100644 --- a/x/leveragelp/keeper/begin_blocker_test.go +++ b/x/leveragelp/keeper/begin_blocker_test.go @@ -1,11 +1,16 @@ package keeper_test import ( + "fmt" "time" "cosmossdk.io/math" "github.com/cometbft/cometbft/crypto/ed25519" sdk "github.com/cosmos/cosmos-sdk/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + "github.com/elys-network/elys/x/leveragelp/types" + stablestakekeeper "github.com/elys-network/elys/x/stablestake/keeper" + stablestaketypes "github.com/elys-network/elys/x/stablestake/types" ) func (suite KeeperTestSuite) TestBeginBlocker() { @@ -64,10 +69,147 @@ func (suite KeeperTestSuite) TestLiquidatePositionIfUnhealthy() { cacheCtx, _ = suite.ctx.CacheContext() position.StopLossPrice = math.LegacyNewDec(100000) - k.SetPosition(cacheCtx, position) + k.SetPosition(cacheCtx, position, sdk.NewInt(0)) underStopLossPrice, earlyReturn := k.ClosePositionIfUnderStopLossPrice(cacheCtx, position, pool, ammPool) suite.Require().True(underStopLossPrice) suite.Require().False(earlyReturn) _, err = k.GetPosition(cacheCtx, position.Address, position.Id) suite.Require().Error(err) } + +func (suite KeeperTestSuite) TestLiquidatePositionSorted() { + k := suite.app.LeveragelpKeeper + addr := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + position, _ := suite.OpenPosition(addr) + + // open positions with other addresses + addr2 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + addr3 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + addr4 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + addr5 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + + usdcTokenTotal := sdk.NewInt64Coin("uusdc", 500000) + usdcToken := sdk.NewInt64Coin("uusdc", 100000) + err := suite.app.BankKeeper.MintCoins(suite.ctx, minttypes.ModuleName, sdk.Coins{usdcTokenTotal}) + suite.Require().NoError(err) + err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, minttypes.ModuleName, addr2, sdk.Coins{usdcToken}) + suite.Require().NoError(err) + err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, minttypes.ModuleName, addr3, sdk.Coins{usdcToken}) + suite.Require().NoError(err) + err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, minttypes.ModuleName, addr4, sdk.Coins{usdcToken}) + suite.Require().NoError(err) + err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, minttypes.ModuleName, addr5, sdk.Coins{usdcToken}) + suite.Require().NoError(err) + + stableMsgServer := stablestakekeeper.NewMsgServerImpl(suite.app.StablestakeKeeper) + _, err = stableMsgServer.Bond(sdk.WrapSDKContext(suite.ctx), &stablestaketypes.MsgBond{ + Creator: addr2.String(), + Amount: sdk.NewInt(100000), + }) + suite.Require().NoError(err) + + position3, err := k.OpenLong(suite.ctx, &types.MsgOpen{ + Creator: addr3.String(), + CollateralAsset: "uusdc", + CollateralAmount: sdk.NewInt(2000), + AmmPoolId: 1, + Leverage: sdk.NewDec(2), + }) + suite.Require().NoError(err) + + position4, err := k.OpenLong(suite.ctx, &types.MsgOpen{ + Creator: addr4.String(), + CollateralAsset: "uusdc", + CollateralAmount: sdk.NewInt(2000), + AmmPoolId: 1, + Leverage: sdk.NewDec(6), + }) + suite.Require().NoError(err) + + position5, err := k.OpenLong(suite.ctx, &types.MsgOpen{ + Creator: addr5.String(), + CollateralAsset: "uusdc", + CollateralAmount: sdk.NewInt(2000), + AmmPoolId: 1, + Leverage: sdk.NewDec(4), + }) + suite.Require().NoError(err) + + ammPool, found := suite.app.AmmKeeper.GetPool(suite.ctx, position3.AmmPoolId) + suite.Require().True(found) + health, err := k.GetPositionHealth(suite.ctx, *position3, ammPool) + suite.Require().NoError(err) + suite.Require().Equal(health.String(), "2.000000000000000000") // slippage disabled on amm + + health, err = k.GetPositionHealth(suite.ctx, *position, ammPool) + suite.Require().NoError(err) + suite.Require().Equal(health.String(), "1.250000000000000000") // slippage disabled on amm + + health, err = k.GetPositionHealth(suite.ctx, *position4, ammPool) + suite.Require().NoError(err) + suite.Require().Equal(health.String(), "1.200000000000000000") // slippage disabled on amm + + health, err = k.GetPositionHealth(suite.ctx, *position5, ammPool) + suite.Require().NoError(err) + suite.Require().Equal(health.String(), "1.333333333333333333") // slippage disabled on amm + + // Check order in list + suite.app.LeveragelpKeeper.IteratePoolPosIdsLiquidationSorted(suite.ctx, position.AmmPoolId, func(posId types.AddressId) bool { + position, _ := k.GetPosition(suite.ctx, posId.Address, posId.Id) + health, _ := k.GetPositionHealth(suite.ctx, position, ammPool) + fmt.Printf("Address: %s, Id: %d, value: %s\n", position.Address, position.Id, health.String()) + return false + }) + + err = k.ProcessAddCollateral(suite.ctx, addr4.String(), position4.Id, sdk.NewInt(1000)) + suite.Require().NoError(err) + + // Check order in list + suite.app.LeveragelpKeeper.IteratePoolPosIdsLiquidationSorted(suite.ctx, position.AmmPoolId, func(posId types.AddressId) bool { + position, _ := k.GetPosition(suite.ctx, posId.Address, posId.Id) + health, _ := k.GetPositionHealth(suite.ctx, position, ammPool) + fmt.Printf("Address: %s, Id: %d, value: %s\n", position.Address, position.Id, health.String()) + return false + }) + + // add more lev + k.OpenConsolidate(suite.ctx, position5, &types.MsgOpen{ + Creator: addr5.String(), + CollateralAsset: "uusdc", + CollateralAmount: sdk.NewInt(1000), + AmmPoolId: 1, + Leverage: sdk.NewDec(4), + }) + suite.Require().NoError(err) + + // Check order in list + suite.app.LeveragelpKeeper.IteratePoolPosIdsLiquidationSorted(suite.ctx, position.AmmPoolId, func(posId types.AddressId) bool { + position, _ := k.GetPosition(suite.ctx, posId.Address, posId.Id) + health, _ := k.GetPositionHealth(suite.ctx, position, ammPool) + fmt.Printf("Address: %s, Id: %d, value: %s\n", position.Address, position.Id, health.String()) + return false + }) + + // Partial close. + var ( + msg = &types.MsgClose{ + Creator: addr5.String(), + Id: position5.Id, + LpAmount: position5.LeveragedLpAmount.Quo(sdk.NewInt(2)), + } + ) + suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour)) + + _, _, err = k.CloseLong(suite.ctx, msg) + suite.Require().NoError(err) + + // Check order in list + suite.app.LeveragelpKeeper.IteratePoolPosIdsLiquidationSorted(suite.ctx, position.AmmPoolId, func(posId types.AddressId) bool { + position, _ := k.GetPosition(suite.ctx, posId.Address, posId.Id) + health, _ := k.GetPositionHealth(suite.ctx, position, ammPool) + fmt.Printf("Address: %s, Id: %d, value: %s\n", position.Address, position.Id, health.String()) + return false + }) +} + +// Add stablestake update hook test diff --git a/x/leveragelp/keeper/hooks_stablestake.go b/x/leveragelp/keeper/hooks_stablestake.go new file mode 100644 index 000000000..ce11df8e3 --- /dev/null +++ b/x/leveragelp/keeper/hooks_stablestake.go @@ -0,0 +1,37 @@ +package keeper + +import ( + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + stablestaketypes "github.com/elys-network/elys/x/stablestake/types" +) + +func (k Keeper) AfterUpdateInterestStacked(ctx sdk.Context, address string, old sdk.Int, new sdk.Int) error { + k.SetSortedLiquidation(ctx, address, old, new) + return nil +} + +// Hooks wrapper struct for incentive keeper +type StableStakeHooks struct { + k Keeper +} + +var _ stablestaketypes.StableStakeHooks = StableStakeHooks{} + +// Return the wrapper struct +func (k Keeper) StableStakeHooks() StableStakeHooks { + return StableStakeHooks{k} +} + +func (h StableStakeHooks) AfterBond(ctx sdk.Context, sender string, shareAmount math.Int) error { + return nil +} + +func (h StableStakeHooks) AfterUnbond(ctx sdk.Context, sender string, shareAmount math.Int) error { + return nil +} + +func (h StableStakeHooks) AfterUpdateInterestStacked(ctx sdk.Context, address string, old sdk.Int, new sdk.Int) error { + h.k.AfterUpdateInterestStacked(ctx, address, old, new) + return nil +} diff --git a/x/leveragelp/keeper/msg_server_update_stop_loss.go b/x/leveragelp/keeper/msg_server_update_stop_loss.go index 897eaceb3..fc746205a 100644 --- a/x/leveragelp/keeper/msg_server_update_stop_loss.go +++ b/x/leveragelp/keeper/msg_server_update_stop_loss.go @@ -34,8 +34,9 @@ func (k msgServer) UpdateStopLoss(goCtx context.Context, msg *types.MsgUpdateSto return nil, err } + debt := k.stableKeeper.GetDebt(ctx, position.GetPositionAddress()) position.StopLossPrice = msg.Price - k.SetPosition(ctx, position) + k.SetPosition(ctx, position, debt.Borrowed.Add(debt.InterestStacked).Sub(debt.InterestPaid)) event := sdk.NewEvent(types.EventOpen, sdk.NewAttribute("id", strconv.FormatInt(int64(position.Id), 10)), diff --git a/x/leveragelp/keeper/position.go b/x/leveragelp/keeper/position.go index ebcd6e9c0..b1fbb034e 100644 --- a/x/leveragelp/keeper/position.go +++ b/x/leveragelp/keeper/position.go @@ -27,7 +27,7 @@ func (k Keeper) GetPosition(ctx sdk.Context, positionAddress string, id uint64) return position, nil } -func (k Keeper) SetPosition(ctx sdk.Context, position *types.Position) { +func (k Keeper) SetPosition(ctx sdk.Context, position *types.Position, oldDebt sdk.Int) { store := ctx.KVStore(k.storeKey) count := k.GetPositionCount(ctx) openCount := k.GetOpenPositionCount(ctx) @@ -43,8 +43,8 @@ func (k Keeper) SetPosition(ctx sdk.Context, position *types.Position) { } else { old, err := k.GetPosition(ctx, position.Address, position.Id) if err == nil { - debt := k.stableKeeper.UpdateInterestStackedByAddress(ctx, old.GetPositionAddress()) - liquidationKey := types.GetLiquidationSortKey(old.AmmPoolId, old.LeveragedLpAmount, debt.Borrowed, old.Id) + // Make sure liability changes are handled properly here, this should always be updated whenever liability is changed + liquidationKey := types.GetLiquidationSortKey(old.AmmPoolId, old.LeveragedLpAmount, oldDebt, old.Id) if len(liquidationKey) > 0 { store.Delete(liquidationKey) } @@ -58,6 +58,9 @@ func (k Keeper) SetPosition(ctx sdk.Context, position *types.Position) { key := types.GetPositionKey(position.Address, position.Id) store.Set(key, k.cdc.MustMarshal(position)) + // for stablestake hook + store.Set([]byte(position.GetPositionAddress()), key) + // Add position sort keys addrId := types.AddressId{ Id: position.Id, @@ -65,7 +68,7 @@ func (k Keeper) SetPosition(ctx sdk.Context, position *types.Position) { } bz := k.cdc.MustMarshal(&addrId) debt := k.stableKeeper.UpdateInterestStackedByAddress(ctx, position.GetPositionAddress()) - liquidationKey := types.GetLiquidationSortKey(position.AmmPoolId, position.LeveragedLpAmount, debt.Borrowed, position.Id) + liquidationKey := types.GetLiquidationSortKey(position.AmmPoolId, position.LeveragedLpAmount, debt.Borrowed.Sub(debt.InterestPaid).Add(debt.InterestStacked), position.Id) if len(liquidationKey) > 0 { store.Set(liquidationKey, bz) } @@ -75,7 +78,7 @@ func (k Keeper) SetPosition(ctx sdk.Context, position *types.Position) { } } -func (k Keeper) DestroyPosition(ctx sdk.Context, positionAddress string, id uint64) error { +func (k Keeper) DestroyPosition(ctx sdk.Context, positionAddress string, id uint64, oldDebt sdk.Int) error { key := types.GetPositionKey(positionAddress, id) store := ctx.KVStore(k.storeKey) if !store.Has(key) { @@ -87,7 +90,7 @@ func (k Keeper) DestroyPosition(ctx sdk.Context, positionAddress string, id uint old, err := k.GetPosition(ctx, positionAddress, id) if err == nil { debt := k.stableKeeper.UpdateInterestStackedByAddress(ctx, old.GetPositionAddress()) - liquidationKey := types.GetLiquidationSortKey(old.AmmPoolId, old.LeveragedLpAmount, debt.Borrowed, old.Id) + liquidationKey := types.GetLiquidationSortKey(old.AmmPoolId, old.LeveragedLpAmount, debt.Borrowed.Sub(debt.InterestPaid).Add(debt.InterestStacked), old.Id) if len(liquidationKey) > 0 { store.Delete(liquidationKey) } @@ -95,6 +98,7 @@ func (k Keeper) DestroyPosition(ctx sdk.Context, positionAddress string, id uint if len(stopLossKey) > 0 { store.Delete(stopLossKey) } + store.Delete([]byte(old.GetPositionAddress())) } // decrement open position count @@ -109,6 +113,60 @@ func (k Keeper) DestroyPosition(ctx sdk.Context, positionAddress string, id uint return nil } +// Set sorted liquidation +func (k Keeper) SetSortedLiquidation(ctx sdk.Context, address string, old sdk.Int, new sdk.Int) { + store := ctx.KVStore(k.storeKey) + if store.Has([]byte(address)) { + key := store.Get([]byte(address)) + if !store.Has(key) { + return + } + res := store.Get(key) + var position types.Position + k.cdc.MustUnmarshal(res, &position) + // Make sure liability changes are handled properly here, this should always be updated whenever liability is changed + liquidationKey := types.GetLiquidationSortKey(position.AmmPoolId, position.LeveragedLpAmount, old, position.Id) + if len(liquidationKey) > 0 && store.Has(liquidationKey) { + store.Delete(liquidationKey) + } + + // Add position sort keys + addrId := types.AddressId{ + Id: position.Id, + Address: position.Address, + } + bz := k.cdc.MustMarshal(&addrId) + liquidationKey = types.GetLiquidationSortKey(position.AmmPoolId, position.LeveragedLpAmount, new, position.Id) + if len(liquidationKey) > 0 { + store.Set(liquidationKey, bz) + } + } +} + +// Change DS for migration +func (k Keeper) SetSortedLiquidationAndStopLoss(ctx sdk.Context, position types.Position) { + store := ctx.KVStore(k.storeKey) + key := types.GetPositionKey(position.Address, position.Id) + // for stablestake hook + store.Set([]byte(position.GetPositionAddress()), key) + + // Add position sort keys + addrId := types.AddressId{ + Id: position.Id, + Address: position.Address, + } + bz := k.cdc.MustMarshal(&addrId) + debt := k.stableKeeper.UpdateInterestStackedByAddress(ctx, position.GetPositionAddress()) + liquidationKey := types.GetLiquidationSortKey(position.AmmPoolId, position.LeveragedLpAmount, debt.Borrowed.Sub(debt.InterestPaid).Add(debt.InterestStacked), position.Id) + if len(liquidationKey) > 0 { + store.Set(liquidationKey, bz) + } + stopLossKey := types.GetStopLossSortKey(position.AmmPoolId, position.StopLossPrice, position.Id) + if len(stopLossKey) > 0 { + store.Set(stopLossKey, bz) + } +} + // Set Open Position count func (k Keeper) SetOpenPositionCount(ctx sdk.Context, count uint64) { store := ctx.KVStore(k.storeKey) @@ -207,6 +265,36 @@ func (k Keeper) IteratePoolPosIdsStopLossSorted(ctx sdk.Context, poolId uint64, } } +func (k Keeper) DeletePoolPosIdsLiquidationSorted(ctx sdk.Context, poolId uint64) { + store := ctx.KVStore(k.storeKey) + iterator := sdk.KVStorePrefixIterator(store, types.GetLiquidationSortPrefix(poolId)) + defer func(iterator sdk.Iterator) { + err := iterator.Close() + if err != nil { + panic(err) + } + }(iterator) + + for ; iterator.Valid(); iterator.Next() { + store.Delete(iterator.Key()) + } +} + +func (k Keeper) DeletePoolPosIdsStopLossSorted(ctx sdk.Context, poolId uint64) { + store := ctx.KVStore(k.storeKey) + iterator := sdk.KVStorePrefixIterator(store, types.GetStopLossSortPrefix(poolId)) + defer func(iterator sdk.Iterator) { + err := iterator.Close() + if err != nil { + panic(err) + } + }(iterator) + + for ; iterator.Valid(); iterator.Next() { + store.Delete(iterator.Key()) + } +} + func (k Keeper) GetPositions(ctx sdk.Context, pagination *query.PageRequest) ([]*types.Position, *query.PageResponse, error) { var positionList []*types.Position store := ctx.KVStore(k.storeKey) diff --git a/x/leveragelp/keeper/position_close.go b/x/leveragelp/keeper/position_close.go index 188994936..a5082ee18 100644 --- a/x/leveragelp/keeper/position_close.go +++ b/x/leveragelp/keeper/position_close.go @@ -18,6 +18,9 @@ func (k Keeper) ForceCloseLong(ctx sdk.Context, position types.Position, pool ty return sdk.ZeroInt(), err } + // Old debt + oldDebt := k.stableKeeper.GetDebt(ctx, position.GetPositionAddress()) + // Repay with interest debt := k.stableKeeper.UpdateInterestStackedByAddress(ctx, position.GetPositionAddress()) @@ -72,12 +75,12 @@ func (k Keeper) ForceCloseLong(ctx sdk.Context, position types.Position, pool ty if err != nil { return sdk.ZeroInt(), err } - err = k.DestroyPosition(ctx, position.Address, position.Id) + err = k.DestroyPosition(ctx, position.Address, position.Id, oldDebt.Borrowed.Add(debt.InterestStacked).Sub(debt.InterestPaid)) if err != nil { return sdk.ZeroInt(), err } } else { - k.SetPosition(ctx, &position) + k.SetPosition(ctx, &position, oldDebt.Borrowed.Add(debt.InterestStacked).Sub(debt.InterestPaid)) } // Hooks after leveragelp position closed diff --git a/x/leveragelp/keeper/position_open.go b/x/leveragelp/keeper/position_open.go index 74b7741d6..67ec2c869 100644 --- a/x/leveragelp/keeper/position_open.go +++ b/x/leveragelp/keeper/position_open.go @@ -100,6 +100,8 @@ func (k Keeper) ProcessOpenLong(ctx sdk.Context, position *types.Position, lever return nil, types.ErrOnlyBaseCurrencyAllowed } + oldDebt := k.stableKeeper.GetDebt(ctx, position.GetPositionAddress()) + // Calculate the leveraged amount based on the collateral provided and the leverage. leveragedAmount := sdk.NewInt(collateralAmountDec.Mul(leverage).TruncateInt().Int64()) @@ -143,7 +145,8 @@ func (k Keeper) ProcessOpenLong(ctx sdk.Context, position *types.Position, lever position.LeveragedLpAmount = position.LeveragedLpAmount.Add(shares) position.Liabilities = position.Liabilities.Add(borrowCoin.Amount) position.PositionHealth = lr - k.SetPosition(ctx, position) + + k.SetPosition(ctx, position, oldDebt.Borrowed.Add(oldDebt.InterestStacked).Sub(oldDebt.InterestPaid)) return position, nil } diff --git a/x/leveragelp/keeper/position_test.go b/x/leveragelp/keeper/position_test.go index 9db52a9e7..1e3e8e2df 100644 --- a/x/leveragelp/keeper/position_test.go +++ b/x/leveragelp/keeper/position_test.go @@ -33,13 +33,43 @@ func TestSetGetPosition(t *testing.T) { PositionHealth: sdk.NewDec(0), Id: 0, } - leveragelp.SetPosition(ctx, &position) + leveragelp.SetPosition(ctx, &position, sdk.NewInt(0)) } positionCount := leveragelp.GetPositionCount(ctx) require.Equal(t, positionCount, (uint64)(2)) } +func TestSetLiquidation(t *testing.T) { + app := simapp.InitElysTestApp(true) + ctx := app.BaseApp.NewContext(true, tmproto.Header{}) + + leveragelp := app.LeveragelpKeeper + + // Generate 2 random accounts with 1000stake balanced + addr := simapp.AddTestAddrs(app, ctx, 2, sdk.NewInt(1000000)) + + for i := 0; i < 2; i++ { + position := types.Position{ + Address: addr[i].String(), + Collateral: sdk.NewCoin(paramtypes.BaseCurrency, sdk.NewInt(0)), + Liabilities: sdk.NewInt(0), + InterestPaid: sdk.NewInt(0), + AmmPoolId: 1, + Leverage: sdk.NewDec(0), + PositionHealth: sdk.NewDec(0), + Id: 0, + } + leveragelp.SetPosition(ctx, &position, sdk.NewInt(0)) + } + + debt := app.StablestakeKeeper.GetDebt(ctx, addr[0]) + leveragelp.SetSortedLiquidation(ctx, addr[0].String(), debt.Borrowed.Add(debt.InterestStacked).Sub(debt.InterestPaid), sdk.NewInt(100)) + + positionCount := leveragelp.GetPositionCount(ctx) + require.Equal(t, positionCount, (uint64)(2)) +} + func TestIteratePoolPosIdsLiquidationSorted(t *testing.T) { app := simapp.InitElysTestApp(true) ctx := app.BaseApp.NewContext(true, tmproto.Header{}) @@ -120,7 +150,7 @@ func TestIteratePoolPosIdsLiquidationSorted(t *testing.T) { LastInterestCalcTime: uint64(ctx.BlockTime().Unix()), } stablestake.SetDebt(ctx, debt) - leveragelp.SetPosition(ctx, &position) + leveragelp.SetPosition(ctx, &position, sdk.NewInt(0)) } idsSorted := []uint64{} @@ -201,7 +231,7 @@ func TestIteratePoolPosIdsStopLossSorted(t *testing.T) { PositionHealth: sdk.NewDec(0), StopLossPrice: math.LegacyDec(info.StopLossPrice), } - leveragelp.SetPosition(ctx, &position) + leveragelp.SetPosition(ctx, &position, sdk.NewInt(0)) } idsSorted := []uint64{} diff --git a/x/leveragelp/keeper/utils_test.go b/x/leveragelp/keeper/utils_test.go index 51f7dcd7c..1740f2a92 100644 --- a/x/leveragelp/keeper/utils_test.go +++ b/x/leveragelp/keeper/utils_test.go @@ -38,7 +38,7 @@ func (suite KeeperTestSuite) TestCheckSameAssets() { SetupStableCoinPrices(suite.ctx, suite.app.OracleKeeper) position := types.NewPosition(addr[0].String(), sdk.NewInt64Coin("USDC", 0), sdk.NewDec(5), 1) - k.SetPosition(suite.ctx, position) + k.SetPosition(suite.ctx, position, sdk.NewInt(0)) msg := &types.MsgOpen{ Creator: addr[0].String(), @@ -47,7 +47,7 @@ func (suite KeeperTestSuite) TestCheckSameAssets() { AmmPoolId: 1, Leverage: sdk.NewDec(1), } - + // Expect no error position = k.CheckSamePosition(suite.ctx, msg) suite.Require().NotNil(position) diff --git a/x/leveragelp/migrations/v4_migration.go b/x/leveragelp/migrations/v4_migration.go index 171decd49..fba1a1b23 100644 --- a/x/leveragelp/migrations/v4_migration.go +++ b/x/leveragelp/migrations/v4_migration.go @@ -7,7 +7,7 @@ import ( func (m Migrator) V4Migration(ctx sdk.Context) error { positions := m.keeper.GetAllPositions(ctx) for _, position := range positions { - m.keeper.SetPosition(ctx, &position) + m.keeper.SetPosition(ctx, &position, sdk.NewInt(0)) } return nil } diff --git a/x/leveragelp/migrations/v7_migration.go b/x/leveragelp/migrations/v7_migration.go new file mode 100644 index 000000000..7f998ade7 --- /dev/null +++ b/x/leveragelp/migrations/v7_migration.go @@ -0,0 +1,58 @@ +package migrations + +import ( + "fmt" + + "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/elys-network/elys/x/leveragelp/types" +) + +func (m Migrator) V7Migration(ctx sdk.Context) error { + // Traverse positions and update lp amount and health + // Update data structure + positions := m.keeper.GetAllPositions(ctx) + pools := m.keeper.GetAllPools(ctx) + for _, pool := range pools { + m.keeper.DeletePoolPosIdsLiquidationSorted(ctx, pool.AmmPoolId) + m.keeper.DeletePoolPosIdsStopLossSorted(ctx, pool.AmmPoolId) + } + for _, position := range positions { + m.keeper.SetSortedLiquidationAndStopLoss(ctx, position) + } + + // Liquidate <1.1 positions + // Q: What will happen if there won't be enough liquidity to return to users(as health for some positions must be below 1) ? Do we need to fill the pool ? + for _, pool := range pools { + ammPool, err := m.keeper.GetAmmPool(ctx, pool.AmmPoolId) + if err != nil { + ctx.Logger().Error(errors.Wrap(err, fmt.Sprintf("error getting amm pool: %d", pool.AmmPoolId)).Error()) + continue + } + m.keeper.IteratePoolPosIdsLiquidationSorted(ctx, pool.AmmPoolId, func(posId types.AddressId) bool { + position, err := m.keeper.GetPosition(ctx, posId.Address, posId.Id) + if err != nil { + return false + } + isHealthy, earlyReturn := m.keeper.LiquidatePositionIfUnhealthy(ctx, &position, pool, ammPool) + if !earlyReturn && isHealthy { + return true + } + return false + }) + + // Close stopLossPrice reached positions + m.keeper.IteratePoolPosIdsStopLossSorted(ctx, pool.AmmPoolId, func(posId types.AddressId) bool { + position, err := m.keeper.GetPosition(ctx, posId.Address, posId.Id) + if err != nil { + return false + } + underStopLossPrice, earlyReturn := m.keeper.ClosePositionIfUnderStopLossPrice(ctx, &position, pool, ammPool) + if !earlyReturn && underStopLossPrice { + return true + } + return false + }) + } + return nil +} diff --git a/x/leveragelp/module.go b/x/leveragelp/module.go index e37c456f5..700b81505 100644 --- a/x/leveragelp/module.go +++ b/x/leveragelp/module.go @@ -117,7 +117,7 @@ func (am AppModule) RegisterServices(cfg module.Configurator) { types.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper)) types.RegisterQueryServer(cfg.QueryServer(), am.keeper) m := migrations.NewMigrator(am.keeper) - err := cfg.RegisterMigration(types.ModuleName, 5, m.V6Migration) + err := cfg.RegisterMigration(types.ModuleName, 6, m.V7Migration) if err != nil { panic(err) } @@ -144,7 +144,7 @@ func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.Raw } // ConsensusVersion is a sequence number for state-breaking change of the module. It should be incremented on each consensus-breaking change introduced by the module. To avoid wrong/empty versions, the initial version should be set to 1 -func (AppModule) ConsensusVersion() uint64 { return 6 } +func (AppModule) ConsensusVersion() uint64 { return 7 } // BeginBlock contains the logic that is automatically triggered at the beginning of each block func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) { diff --git a/x/leveragelp/types/keys.go b/x/leveragelp/types/keys.go index c12038e88..bfae5e2ea 100644 --- a/x/leveragelp/types/keys.go +++ b/x/leveragelp/types/keys.go @@ -2,6 +2,7 @@ package types import ( "encoding/binary" + "strconv" "cosmossdk.io/math" ) @@ -69,11 +70,33 @@ func GetLiquidationSortKey(poolId uint64, lpAmount math.Int, borrowed math.Int, return []byte{} } + // default precision is 18 + // final string = decimalvalue + positionId(consistentlength) sortDec := math.LegacyNewDecFromInt(lpAmount).QuoInt(borrowed) - bytes := sortDec.BigInt().Bytes() - lengthPrefix := GetUint64Bytes(uint64(len(bytes))) - posIdSuffix := GetUint64Bytes(id) - return append(append(append(poolIdPrefix, lengthPrefix...), bytes...), posIdSuffix...) + paddedPosition := IntToStringWithPadding(id) + bytes := []byte(sortDec.String() + paddedPosition) + return append(poolIdPrefix, bytes...) +} + +func IntToStringWithPadding(position uint64) string { + // Define the desired length of the output string + const length = 9 + + // Convert the integer to a string + str := strconv.FormatUint(position, 18) + + // Calculate the number of leading zeros needed + padding := length - len(str) + + // Create the leading zeros string + leadingZeros := "" + for i := 0; i < padding; i++ { + leadingZeros += "0" + } + + // Concatenate leading zeros with the original number string + result := leadingZeros + str + return result } func GetStopLossSortPrefix(poolId uint64) []byte { diff --git a/x/masterchef/keeper/hooks_stablestake.go b/x/masterchef/keeper/hooks_stablestake.go index e4283126c..e7e0e6246 100644 --- a/x/masterchef/keeper/hooks_stablestake.go +++ b/x/masterchef/keeper/hooks_stablestake.go @@ -27,3 +27,7 @@ func (h StableStakeHooks) AfterUnbond(ctx sdk.Context, sender string, shareAmoun h.k.AfterWithdraw(ctx, stablestaketypes.PoolId, sender, shareAmount) return nil } + +func (h StableStakeHooks) AfterUpdateInterestStacked(ctx sdk.Context, address string, old sdk.Int, new sdk.Int) error { + return nil +} diff --git a/x/stablestake/keeper/begin_blocker.go b/x/stablestake/keeper/begin_blocker.go index 90bff25f0..42952e7e2 100644 --- a/x/stablestake/keeper/begin_blocker.go +++ b/x/stablestake/keeper/begin_blocker.go @@ -10,6 +10,7 @@ func (k Keeper) BeginBlocker(ctx sdk.Context) { epochPosition := k.GetEpochPosition(ctx, epochLength) if epochPosition == 0 { // if epoch has passed + // TODO: divide them in blocks, update values params := k.GetParams(ctx) rate := k.InterestRateComputation(ctx) params.InterestRate = rate @@ -17,7 +18,9 @@ func (k Keeper) BeginBlocker(ctx sdk.Context) { debts := k.AllDebts(ctx) for _, debt := range debts { + old := debt.Borrowed.Add(debt.InterestStacked).Sub(debt.InterestPaid) k.UpdateInterestStacked(ctx, debt) + k.hooks.AfterUpdateInterestStacked(ctx, debt.Address, old, debt.Borrowed.Add(debt.InterestStacked).Sub(debt.InterestPaid)) } } } diff --git a/x/stablestake/keeper/hooks.go b/x/stablestake/keeper/hooks.go index c0789f5ca..bba5d9645 100644 --- a/x/stablestake/keeper/hooks.go +++ b/x/stablestake/keeper/hooks.go @@ -36,3 +36,14 @@ func (mh MultiStableStakeHooks) AfterUnbond(ctx sdk.Context, sender string, shar } return nil } + +// Committed is called when staker committed his token +func (mh MultiStableStakeHooks) AfterUpdateInterestStacked(ctx sdk.Context, address string, old math.Int, new math.Int) error { + for i := range mh { + err := mh[i].AfterUpdateInterestStacked(ctx, address, old, new) + if err != nil { + return err + } + } + return nil +} diff --git a/x/stablestake/types/interfaces.go b/x/stablestake/types/interfaces.go index 2dc90ded9..deb4cd83f 100644 --- a/x/stablestake/types/interfaces.go +++ b/x/stablestake/types/interfaces.go @@ -9,4 +9,5 @@ import ( type StableStakeHooks interface { AfterBond(ctx sdk.Context, sender string, shareAmount math.Int) error AfterUnbond(ctx sdk.Context, sender string, shareAmount math.Int) error + AfterUpdateInterestStacked(ctx sdk.Context, address string, old math.Int, new math.Int) error } diff --git a/x/tier/keeper/hooks_stable_stake.go b/x/tier/keeper/hooks_stable_stake.go index 918aea292..09d103c5d 100644 --- a/x/tier/keeper/hooks_stable_stake.go +++ b/x/tier/keeper/hooks_stable_stake.go @@ -37,3 +37,7 @@ func (h StableStakeHooks) AfterUnbond(ctx sdk.Context, sender string, shareAmoun h.k.AfterUnbond(ctx, sender, shareAmount) return nil } + +func (h StableStakeHooks) AfterUpdateInterestStacked(ctx sdk.Context, address string, old sdk.Int, new sdk.Int) error { + return nil +}