diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index 46ef4cc1f..4282fb6f9 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -705,6 +705,8 @@ func NewAppKeeper( app.StablestakeKeeper.SetHooks(stablestakekeeper.NewMultiStableStakeHooks( app.MasterchefKeeper.StableStakeHooks(), app.TierKeeper.StableStakeHooks(), + // unbonding checks leverage pool health + app.LeveragelpKeeper.StableStakeHooks(), )) app.LeveragelpKeeper.SetHooks(leveragelpmoduletypes.NewMultiLeverageLpHooks( diff --git a/x/leveragelp/keeper/hooks_stablestake.go b/x/leveragelp/keeper/hooks_stablestake.go new file mode 100644 index 000000000..6cb0d9dd4 --- /dev/null +++ b/x/leveragelp/keeper/hooks_stablestake.go @@ -0,0 +1,49 @@ +package keeper + +import ( + "fmt" + + errorsmod "cosmossdk.io/errors" + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/elys-network/elys/x/leveragelp/types" + stablestaketypes "github.com/elys-network/elys/x/stablestake/types" +) + +func (k Keeper) AfterBond(ctx sdk.Context, sender sdk.AccAddress, shareAmount math.Int) error { + return nil +} + +func (k Keeper) AfterUnbond(ctx sdk.Context, sender sdk.AccAddress, shareAmount math.Int) error { + // loop over all leverage pools and throw error if any leverage pool is unhealthy + for _, pool := range k.GetAllPools(ctx) { + // print that after unbound is called and checking pool health of pool id + fmt.Printf("after unbound is called and checking pool health of pool id %d\n", pool.AmmPoolId) + if err := k.CheckPoolHealth(ctx, pool.AmmPoolId); err != nil { + return errorsmod.Wrapf(types.ErrUnbondingPoolHealth, "pool health too low to unbond for pool %d", pool.AmmPoolId) + } + } + + return nil +} + +type StableStakeHooks struct { + k Keeper +} + +var _ stablestaketypes.StableStakeHooks = StableStakeHooks{} + +// Return the wrapper struct +func (k Keeper) StableStakeHooks() StableStakeHooks { + return StableStakeHooks{k} +} + +// AfterPoolCreated is called after CreatePool +func (h StableStakeHooks) AfterBond(ctx sdk.Context, sender sdk.AccAddress, shareAmount math.Int) error { + return h.k.AfterBond(ctx, sender, shareAmount) +} + +// AfterJoinPool is called after JoinPool, JoinSwapExternAmountIn, and JoinSwapShareAmountOut +func (h StableStakeHooks) AfterUnbond(ctx sdk.Context, sender sdk.AccAddress, shareAmount math.Int) error { + return h.k.AfterUnbond(ctx, sender, shareAmount) +} diff --git a/x/leveragelp/keeper/msg_server_open_test.go b/x/leveragelp/keeper/msg_server_open_test.go index 7d386052f..89938e264 100644 --- a/x/leveragelp/keeper/msg_server_open_test.go +++ b/x/leveragelp/keeper/msg_server_open_test.go @@ -94,6 +94,7 @@ func (suite *KeeperTestSuite) TestOpen_PoolWithBaseCurrencyAsset() { expectErr bool expectErrMsg string prerequisiteFunction func() + postValidateFunc func(msg *types.MsgOpen) }{ {"failed user authorization", &types.MsgOpen{ @@ -109,6 +110,7 @@ func (suite *KeeperTestSuite) TestOpen_PoolWithBaseCurrencyAsset() { func() { suite.EnableWhiteListing() }, + func(msg *types.MsgOpen) {}, }, {"no positions allowed", &types.MsgOpen{ @@ -125,6 +127,7 @@ func (suite *KeeperTestSuite) TestOpen_PoolWithBaseCurrencyAsset() { suite.DisableWhiteListing() suite.SetMaxOpenPositions(0) }, + func(msg *types.MsgOpen) {}, }, {name: "Max positions reached", input: &types.MsgOpen{ @@ -139,6 +142,7 @@ func (suite *KeeperTestSuite) TestOpen_PoolWithBaseCurrencyAsset() { expectErrMsg: "cannot open new positions, open positions 0 - max positions 0: max open", prerequisiteFunction: func() { }, + postValidateFunc: func(msg *types.MsgOpen) {}, }, {name: "Pool not found", input: &types.MsgOpen{ @@ -154,6 +158,7 @@ func (suite *KeeperTestSuite) TestOpen_PoolWithBaseCurrencyAsset() { prerequisiteFunction: func() { suite.SetMaxOpenPositions(3) }, + postValidateFunc: func(msg *types.MsgOpen) {}, }, {name: "base currency not found", input: &types.MsgOpen{ @@ -172,6 +177,7 @@ func (suite *KeeperTestSuite) TestOpen_PoolWithBaseCurrencyAsset() { suite.RemovePrices(suite.ctx, []string{"uusdc"}) suite.SetMaxOpenPositions(20) }, + postValidateFunc: func(msg *types.MsgOpen) {}, }, {name: "AMM Pool not found", input: &types.MsgOpen{ @@ -187,6 +193,7 @@ func (suite *KeeperTestSuite) TestOpen_PoolWithBaseCurrencyAsset() { prerequisiteFunction: func() { suite.SetupCoinPrices(suite.ctx) }, + postValidateFunc: func(msg *types.MsgOpen) {}, }, {name: "Pool not enabled", input: &types.MsgOpen{ @@ -205,6 +212,7 @@ func (suite *KeeperTestSuite) TestOpen_PoolWithBaseCurrencyAsset() { amm_pool := ammtypes.Pool{PoolId: 2, Address: ammtypes.NewPoolAddress(2).String(), TotalShares: sdk.Coin{Amount: sdkmath.NewInt(100)}} suite.app.AmmKeeper.SetPool(suite.ctx, amm_pool) }, + postValidateFunc: func(msg *types.MsgOpen) {}, }, {"Collateral asset not equal to base currency", &types.MsgOpen{ @@ -220,6 +228,7 @@ func (suite *KeeperTestSuite) TestOpen_PoolWithBaseCurrencyAsset() { func() { suite.SetPoolThreshold(sdkmath.LegacyMustNewDecFromStr("0.2")) }, + func(msg *types.MsgOpen) {}, }, {"Base currency not found", &types.MsgOpen{ @@ -234,6 +243,7 @@ func (suite *KeeperTestSuite) TestOpen_PoolWithBaseCurrencyAsset() { types.ErrOnlyBaseCurrencyAllowed.Error(), func() { }, + func(msg *types.MsgOpen) {}, }, {"First open position but leads to low pool health", &types.MsgOpen{ @@ -250,6 +260,7 @@ func (suite *KeeperTestSuite) TestOpen_PoolWithBaseCurrencyAsset() { suite.AddCoinPrices(suite.ctx, []string{ptypes.BaseCurrency}) suite.SetPoolThreshold(sdkmath.LegacyOneDec()) }, + func(msg *types.MsgOpen) {}, }, {"Low Balance of creator", &types.MsgOpen{ @@ -265,6 +276,7 @@ func (suite *KeeperTestSuite) TestOpen_PoolWithBaseCurrencyAsset() { func() { suite.SetPoolThreshold(sdkmath.LegacyMustNewDecFromStr("0.2")) }, + func(msg *types.MsgOpen) {}, }, {"Borrowing more than allowed", &types.MsgOpen{ @@ -280,6 +292,7 @@ func (suite *KeeperTestSuite) TestOpen_PoolWithBaseCurrencyAsset() { func() { suite.SetPoolThreshold(sdkmath.LegacyMustNewDecFromStr("0.2")) }, + func(msg *types.MsgOpen) {}, }, {"Position safety factor too low", &types.MsgOpen{ @@ -295,6 +308,7 @@ func (suite *KeeperTestSuite) TestOpen_PoolWithBaseCurrencyAsset() { func() { suite.SetSafetyFactor(sdkmath.LegacyOneDec().MulInt64(10)) }, + func(msg *types.MsgOpen) {}, }, {"Open new Position with leverage <=1", &types.MsgOpen{ @@ -309,6 +323,7 @@ func (suite *KeeperTestSuite) TestOpen_PoolWithBaseCurrencyAsset() { "", func() { }, + func(msg *types.MsgOpen) {}, }, {"Open Position", &types.MsgOpen{ @@ -328,6 +343,7 @@ func (suite *KeeperTestSuite) TestOpen_PoolWithBaseCurrencyAsset() { suite.SetSafetyFactor(sdkmath.LegacyMustNewDecFromStr("1.1")) suite.SetPoolThreshold(sdkmath.LegacyMustNewDecFromStr("0.2")) }, + func(msg *types.MsgOpen) {}, }, {"Add on already open position Long but with different leverage 10", &types.MsgOpen{ @@ -342,6 +358,7 @@ func (suite *KeeperTestSuite) TestOpen_PoolWithBaseCurrencyAsset() { "", func() { }, + func(msg *types.MsgOpen) {}, }, {"Add on already open position Long but with different leverage 20", &types.MsgOpen{ @@ -356,6 +373,7 @@ func (suite *KeeperTestSuite) TestOpen_PoolWithBaseCurrencyAsset() { "", func() { }, + func(msg *types.MsgOpen) {}, }, {"Add on already open position Long but with different leverage 1, increase position health", &types.MsgOpen{ @@ -371,6 +389,7 @@ func (suite *KeeperTestSuite) TestOpen_PoolWithBaseCurrencyAsset() { func() { suite.SetSafetyFactor(sdkmath.LegacyMustNewDecFromStr("2.0")) }, + func(msg *types.MsgOpen) {}, }, {"Add on already open position Long but with different leverage 30", &types.MsgOpen{ @@ -385,6 +404,7 @@ func (suite *KeeperTestSuite) TestOpen_PoolWithBaseCurrencyAsset() { types.ErrPositionUnhealthy.Error(), func() { }, + func(msg *types.MsgOpen) {}, }, {"Low pool health to open position", &types.MsgOpen{ @@ -401,6 +421,35 @@ func (suite *KeeperTestSuite) TestOpen_PoolWithBaseCurrencyAsset() { suite.SetSafetyFactor(sdkmath.LegacyMustNewDecFromStr("1.0")) suite.SetPoolThreshold(sdkmath.LegacyOneDec()) }, + func(msg *types.MsgOpen) {}, + }, + {"Unbond with position open", + &types.MsgOpen{ + Creator: addresses[0].String(), + CollateralAsset: ptypes.BaseCurrency, + CollateralAmount: sdkmath.NewInt(100_000_000), + AmmPoolId: 1, + Leverage: sdkmath.LegacyMustNewDecFromStr("10.0"), + StopLossPrice: sdkmath.LegacyMustNewDecFromStr("50.0"), + }, + false, + "", + func() { + suite.ResetSuite() + suite.SetupCoinPrices(suite.ctx) + initializeForOpen(suite, addresses, asset1, asset2) + suite.SetSafetyFactor(sdkmath.LegacyMustNewDecFromStr("0.1")) + suite.SetPoolThreshold(sdkmath.LegacyMustNewDecFromStr("0.2")) + }, + func(msg *types.MsgOpen) { + amount := sdkmath.NewInt(500_000_000_000_000) + stableStakeMsgServer := stablekeeper.NewMsgServerImpl(*suite.app.StablestakeKeeper) + _, err := stableStakeMsgServer.Unbond(suite.ctx, &stabletypes.MsgUnbond{ + Creator: addresses[1].String(), + Amount: amount, + }) + suite.Require().NoError(err) + }, }, } @@ -413,6 +462,7 @@ func (suite *KeeperTestSuite) TestOpen_PoolWithBaseCurrencyAsset() { suite.Require().Error(err) suite.Require().Contains(err.Error(), tc.expectErrMsg) } else { + tc.postValidateFunc(tc.input) // The new value of the portfolio after the hook is called. portfolio_new, _ := suite.app.TierKeeper.GetPortfolio(suite.ctx, sdk.MustAccAddressFromBech32(tc.input.Creator), suite.app.TierKeeper.GetDateFromContext(suite.ctx)) // Initially, there were no entries for the portfolio diff --git a/x/leveragelp/keeper/utils.go b/x/leveragelp/keeper/utils.go index f593f366a..9543b45d5 100644 --- a/x/leveragelp/keeper/utils.go +++ b/x/leveragelp/keeper/utils.go @@ -43,6 +43,9 @@ func (k Keeper) CheckPoolHealth(ctx sdk.Context, poolId uint64) error { return errorsmod.Wrap(types.ErrInvalidBorrowingAsset, "invalid collateral asset") } + // print pool health + fmt.Printf("pool health: %s\n", pool.Health.String()) + if !pool.Health.IsNil() && pool.Health.LTE(k.GetPoolOpenThreshold(ctx)) { return errorsmod.Wrap(types.ErrInvalidPosition, "pool health too low to open new positions") } diff --git a/x/leveragelp/types/errors.go b/x/leveragelp/types/errors.go index 716567a31..f1a8df661 100644 --- a/x/leveragelp/types/errors.go +++ b/x/leveragelp/types/errors.go @@ -34,4 +34,5 @@ var ( ErrPoolLeverageAmountNotZero = errorsmod.Register(ModuleName, 41, "pool leverage amount is greater than zero") ErrLeverageTooSmall = errorsmod.Register(ModuleName, 42, "leverage should be more than or equal to 1") ErrMaxLeverageLpExists = errorsmod.Register(ModuleName, 43, "pool is already leveraged at maximum value") + ErrUnbondingPoolHealth = errorsmod.Register(ModuleName, 44, "pool health too low to unbond") ) diff --git a/x/stablestake/keeper/msg_server_unbond.go b/x/stablestake/keeper/msg_server_unbond.go index 69bd0c2bd..589d2031f 100644 --- a/x/stablestake/keeper/msg_server_unbond.go +++ b/x/stablestake/keeper/msg_server_unbond.go @@ -2,6 +2,7 @@ package keeper import ( "context" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/elys-network/elys/x/stablestake/types" ) @@ -9,6 +10,10 @@ import ( func (k msgServer) Unbond(goCtx context.Context, msg *types.MsgUnbond) (*types.MsgUnbondResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) + return k.Keeper.Unbond(ctx, msg) +} + +func (k Keeper) Unbond(ctx sdk.Context, msg *types.MsgUnbond) (*types.MsgUnbondResponse, error) { params := k.GetParams(ctx) creator := sdk.MustAccAddressFromBech32(msg.Creator) redemptionRate := k.GetRedemptionRate(ctx) diff --git a/x/stablestake/types/expected_keepers.go b/x/stablestake/types/expected_keepers.go index 41f434d4b..a3e58398f 100644 --- a/x/stablestake/types/expected_keepers.go +++ b/x/stablestake/types/expected_keepers.go @@ -2,6 +2,7 @@ package types import ( "context" + sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" assetprofiletypes "github.com/elys-network/elys/x/assetprofile/types"