diff --git a/x/leveragelp/keeper/position_close.go b/x/leveragelp/keeper/position_close.go index 7a06c63cc..99bd31f66 100644 --- a/x/leveragelp/keeper/position_close.go +++ b/x/leveragelp/keeper/position_close.go @@ -52,9 +52,11 @@ func (k Keeper) ForceCloseLong(ctx sdk.Context, position types.Position, pool ty positionOwner := sdk.MustAccAddressFromBech32(position.Address) + // TODO This means bot failed to close position on time, need to forcefully close the position if userAmount.IsNegative() { return math.ZeroInt(), types.ErrNegUserAmountAfterRepay } + if userAmount.IsPositive() { err = k.bankKeeper.SendCoins(ctx, position.GetPositionAddress(), positionOwner, sdk.Coins{sdk.NewCoin(position.Collateral.Denom, userAmount)}) if err != nil { diff --git a/x/perpetual/client/cli/query_close_estimation.go b/x/perpetual/client/cli/query_close_estimation.go index d656fe458..5014a85cf 100644 --- a/x/perpetual/client/cli/query_close_estimation.go +++ b/x/perpetual/client/cli/query_close_estimation.go @@ -2,7 +2,7 @@ package cli import ( "cosmossdk.io/math" - "fmt" + "errors" "strconv" "github.com/cosmos/cosmos-sdk/client" @@ -29,7 +29,7 @@ func CmdCloseEstimation() *cobra.Command { reqClosingAmount, ok := math.NewIntFromString(args[2]) if !ok { - return fmt.Errorf("invalid closing amount") + return errors.New("invalid closing amount") } clientCtx, err := client.GetClientQueryContext(cmd) diff --git a/x/perpetual/keeper/close.go b/x/perpetual/keeper/close.go index c1a96e404..88effd23a 100644 --- a/x/perpetual/keeper/close.go +++ b/x/perpetual/keeper/close.go @@ -1,35 +1,46 @@ package keeper import ( - errorsmod "cosmossdk.io/errors" - sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" - assetprofiletypes "github.com/elys-network/elys/x/assetprofile/types" - ptypes "github.com/elys-network/elys/x/parameter/types" "github.com/elys-network/elys/x/perpetual/types" + "strconv" ) func (k Keeper) Close(ctx sdk.Context, msg *types.MsgClose) (*types.MsgCloseResponse, error) { - entry, found := k.assetProfileKeeper.GetEntry(ctx, ptypes.BaseCurrency) - if !found { - return nil, errorsmod.Wrapf(assetprofiletypes.ErrAssetProfileNotFound, "asset %s not found", ptypes.BaseCurrency) + closedMtp, repayAmount, closingRatio, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, allInterestsPaid, forceClosed, err := k.ClosePosition(ctx, msg) + if err != nil { + return nil, err } - baseCurrency := entry.Denom - closedMtp, repayAmount, closingRatio, err := k.ClosePosition(ctx, msg, baseCurrency) + tradingAssetPrice, err := k.GetAssetPrice(ctx, closedMtp.TradingAsset) if err != nil { return nil, err } - // Emit close event - k.EmitCloseEvent(ctx, closedMtp, repayAmount, closingRatio) + ctx.EventManager().EmitEvent( + sdk.NewEvent(types.EventClose, + sdk.NewAttribute("mtp_id", strconv.FormatInt(int64(closedMtp.Id), 10)), + sdk.NewAttribute("owner", closedMtp.Address), + sdk.NewAttribute("amm_pool_id", strconv.FormatInt(int64(closedMtp.AmmPoolId), 10)), + sdk.NewAttribute("collateral_asset", closedMtp.CollateralAsset), + sdk.NewAttribute("position", closedMtp.Position.String()), + sdk.NewAttribute("mtp_health", closedMtp.MtpHealth.String()), // should be there if it's partial close + sdk.NewAttribute("repay_amount", repayAmount.String()), + sdk.NewAttribute("return_amount", returnAmt.String()), + sdk.NewAttribute("funding_fee_amount", fundingFeeAmt.String()), + sdk.NewAttribute("funding_amount_distributed", fundingAmtDistributed.String()), + sdk.NewAttribute("interest_amount", interestAmt.String()), + sdk.NewAttribute("insurance_amount", insuranceAmt.String()), + sdk.NewAttribute("funding_fee_paid_custody", closedMtp.FundingFeePaidCustody.String()), + sdk.NewAttribute("funding_fee_received_custody", closedMtp.FundingFeeReceivedCustody.String()), + sdk.NewAttribute("closing_ratio", closingRatio.String()), + sdk.NewAttribute("trading_asset_price", tradingAssetPrice.String()), + sdk.NewAttribute("all_interests_paid", strconv.FormatBool(allInterestsPaid)), // Funding Fee is fully paid but interest amount is only partially paid then this will be false + sdk.NewAttribute("force_closed", strconv.FormatBool(forceClosed)), + )) return &types.MsgCloseResponse{ Id: closedMtp.Id, Amount: repayAmount, }, nil } - -func (k Keeper) EmitCloseEvent(ctx sdk.Context, mtp *types.MTP, repayAmount sdkmath.Int, closingRatio sdkmath.LegacyDec) { - ctx.EventManager().EmitEvent(types.GenerateCloseEvent(mtp, repayAmount, closingRatio)) -} diff --git a/x/perpetual/keeper/close_position.go b/x/perpetual/keeper/close_position.go index 8e103561e..d889d553e 100644 --- a/x/perpetual/keeper/close_position.go +++ b/x/perpetual/keeper/close_position.go @@ -1,44 +1,40 @@ package keeper import ( - "fmt" - errorsmod "cosmossdk.io/errors" "cosmossdk.io/math" + "fmt" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/elys-network/elys/x/perpetual/types" ) -func (k Keeper) ClosePosition(ctx sdk.Context, msg *types.MsgClose, baseCurrency string) (*types.MTP, math.Int, math.LegacyDec, error) { +func (k Keeper) ClosePosition(ctx sdk.Context, msg *types.MsgClose) (types.MTP, math.Int, math.LegacyDec, math.Int, math.Int, math.Int, math.Int, math.Int, bool, bool, error) { // Retrieve MTP creator := sdk.MustAccAddressFromBech32(msg.Creator) mtp, err := k.GetMTP(ctx, creator, msg.Id) if err != nil { - return nil, math.ZeroInt(), math.LegacyZeroDec(), err + return types.MTP{}, math.ZeroInt(), math.LegacyZeroDec(), math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), false, false, err } - // Retrieve AmmPool - ammPool, err := k.GetAmmPool(ctx, mtp.AmmPoolId) - if err != nil { - return nil, math.ZeroInt(), math.LegacyZeroDec(), err - } - - // This needs to be updated here to check user doesn't send more than required amount - k.UpdateMTPBorrowInterestUnpaidLiability(ctx, &mtp) - // Retrieve Pool pool, found := k.GetPool(ctx, mtp.AmmPoolId) if !found { - return nil, math.ZeroInt(), math.LegacyZeroDec(), errorsmod.Wrap(types.ErrPoolDoesNotExist, fmt.Sprintf("poolId: %d", mtp.AmmPoolId)) + return mtp, math.ZeroInt(), math.LegacyZeroDec(), math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), false, false, errorsmod.Wrap(types.ErrPoolDoesNotExist, fmt.Sprintf("poolId: %d", mtp.AmmPoolId)) } - // Handle Borrow Interest if within epoch position SettleMTPBorrowInterestUnpaidLiability settles interest using mtp.Custody, mtp.Custody gets reduced - if _, err = k.SettleMTPBorrowInterestUnpaidLiability(ctx, &mtp, &pool, ammPool); err != nil { - return nil, math.ZeroInt(), math.LegacyZeroDec(), err + // Retrieve AmmPool + ammPool, err := k.GetAmmPool(ctx, mtp.AmmPoolId) + if err != nil { + return mtp, math.ZeroInt(), math.LegacyZeroDec(), math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), false, false, err } - err = k.SettleFunding(ctx, &mtp, &pool, ammPool) + // this also handles edge case where bot is unable to close position in time. + repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, allInterestsPaid, forceClosed, err := k.MTPTriggerChecksAndUpdates(ctx, &mtp, &pool, &ammPool) if err != nil { - return nil, math.ZeroInt(), math.LegacyZeroDec(), errorsmod.Wrapf(err, "error handling funding fee") + return types.MTP{}, math.ZeroInt(), math.LegacyZeroDec(), math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), false, false, err + } + + if forceClosed { + return mtp, repayAmt, math.LegacyOneDec(), returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, allInterestsPaid, forceClosed, nil } // Should be declared after SettleMTPBorrowInterestUnpaidLiability and settling funding @@ -51,9 +47,9 @@ func (k Keeper) ClosePosition(ctx sdk.Context, msg *types.MsgClose, baseCurrency } // Estimate swap and repay - repayAmt, err := k.EstimateAndRepay(ctx, &mtp, &pool, &ammPool, baseCurrency, closingRatio) + repayAmt, returnAmt, err = k.EstimateAndRepay(ctx, &mtp, &pool, &ammPool, closingRatio) if err != nil { - return nil, math.ZeroInt(), math.LegacyZeroDec(), err + return mtp, math.ZeroInt(), math.LegacyZeroDec(), math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), allInterestsPaid, forceClosed, err } // EpochHooks after perpetual position closed @@ -61,9 +57,9 @@ func (k Keeper) ClosePosition(ctx sdk.Context, msg *types.MsgClose, baseCurrency params := k.GetParams(ctx) err = k.hooks.AfterPerpetualPositionClosed(ctx, ammPool, pool, creator, params.EnableTakeProfitCustodyLiabilities) if err != nil { - return nil, math.Int{}, math.LegacyDec{}, err + return mtp, math.Int{}, math.LegacyDec{}, math.Int{}, math.Int{}, math.Int{}, math.Int{}, math.Int{}, allInterestsPaid, forceClosed, err } } - return &mtp, repayAmt, closingRatio, nil + return mtp, repayAmt, closingRatio, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, allInterestsPaid, forceClosed, nil } diff --git a/x/perpetual/keeper/close_test.go b/x/perpetual/keeper/close_test.go index 74aafb067..ad2f3b02e 100644 --- a/x/perpetual/keeper/close_test.go +++ b/x/perpetual/keeper/close_test.go @@ -59,7 +59,7 @@ func (suite *PerpetualKeeperTestSuite) TestClose() { Amount: math.NewInt(500), } }, - "asset uusdc not found", + "unable to find base currency entry", math.NewInt(0), }, { @@ -279,7 +279,7 @@ func (suite *PerpetualKeeperTestSuite) TestClose() { math.NewInt(4501), }, { - "Close with too much unpaid Liability to make custody amount 0", + "Force Close with too much unpaid Liability making custody amount 0", func() *types.MsgClose { suite.ResetSuite() @@ -313,8 +313,8 @@ func (suite *PerpetualKeeperTestSuite) TestClose() { Amount: math.NewInt(399), } }, - "error handling funding fee", - math.NewInt(0), + "", + math.NewInt(203), }, { "Close short with Not Enough liquidity", @@ -364,7 +364,7 @@ func (suite *PerpetualKeeperTestSuite) TestClose() { suite.Require().Contains(err.Error(), tc.expectedErrMsg) } else { suite.Require().NoError(err) - suite.Require().Equal(tc.repayAmount, res.Amount) + suite.Require().Equal(res.Amount.String(), tc.repayAmount.String()) } }) } diff --git a/x/perpetual/keeper/estimate_and_repay.go b/x/perpetual/keeper/estimate_and_repay.go index 01696114f..42c44db3d 100644 --- a/x/perpetual/keeper/estimate_and_repay.go +++ b/x/perpetual/keeper/estimate_and_repay.go @@ -3,35 +3,44 @@ package keeper import ( "fmt" + errorsmod "cosmossdk.io/errors" "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" ammtypes "github.com/elys-network/elys/x/amm/types" + assetprofiletypes "github.com/elys-network/elys/x/assetprofile/types" + ptypes "github.com/elys-network/elys/x/parameter/types" "github.com/elys-network/elys/x/perpetual/types" ) // EstimateAndRepay ammPool has to be pointer because RemoveFromPoolBalance (in Repay) updates pool assets // Important to send pointer mtp and pool -func (k Keeper) EstimateAndRepay(ctx sdk.Context, mtp *types.MTP, pool *types.Pool, ammPool *ammtypes.Pool, baseCurrency string, closingRatio math.LegacyDec) (math.Int, error) { +func (k Keeper) EstimateAndRepay(ctx sdk.Context, mtp *types.MTP, pool *types.Pool, ammPool *ammtypes.Pool, closingRatio math.LegacyDec) (math.Int, math.Int, error) { if closingRatio.LTE(math.LegacyZeroDec()) || closingRatio.GT(math.LegacyOneDec()) { - return math.Int{}, fmt.Errorf("invalid closing ratio (%s)", closingRatio.String()) + return math.Int{}, math.Int{}, fmt.Errorf("invalid closing ratio (%s)", closingRatio.String()) } repayAmount, payingLiabilities, _, _, err := k.CalcRepayAmount(ctx, mtp, ammPool, closingRatio) if err != nil { - return math.ZeroInt(), err + return math.ZeroInt(), math.ZeroInt(), err } returnAmount, err := k.CalcReturnAmount(*mtp, repayAmount, closingRatio) if err != nil { - return math.ZeroInt(), err + return math.ZeroInt(), math.ZeroInt(), err } + entry, found := k.assetProfileKeeper.GetEntry(ctx, ptypes.BaseCurrency) + if !found { + return math.Int{}, math.Int{}, errorsmod.Wrapf(assetprofiletypes.ErrAssetProfileNotFound, "asset %s not found", ptypes.BaseCurrency) + } + baseCurrency := entry.Denom + // Note: Long settlement is done in trading asset. And short settlement in usdc in Repay function if err = k.Repay(ctx, mtp, pool, ammPool, returnAmount, payingLiabilities, closingRatio, baseCurrency); err != nil { - return math.ZeroInt(), err + return math.ZeroInt(), math.ZeroInt(), err } - return repayAmount, nil + return repayAmount, returnAmount, nil } // CalcRepayAmount repay amount is in custody asset for liabilities with closing ratio diff --git a/x/perpetual/keeper/events.go b/x/perpetual/keeper/events.go index 3292c3600..894afe7b6 100644 --- a/x/perpetual/keeper/events.go +++ b/x/perpetual/keeper/events.go @@ -8,34 +8,24 @@ import ( "github.com/elys-network/elys/x/perpetual/types" ) -func (k Keeper) EmitFundPayment(ctx sdk.Context, mtp *types.MTP, takeAmount math.Int, takeAsset string, paymentType string) { - ctx.EventManager().EmitEvent(sdk.NewEvent(paymentType, - sdk.NewAttribute("id", strconv.FormatInt(int64(mtp.Id), 10)), - sdk.NewAttribute("payment_amount", takeAmount.String()), - sdk.NewAttribute("payment_asset", takeAsset), - )) -} - -func (k Keeper) EmitForceClose(ctx sdk.Context, eventType string, mtp *types.MTP, repayAmount math.Int, closer string) { - ctx.EventManager().EmitEvent(sdk.NewEvent(eventType, - sdk.NewAttribute("id", strconv.FormatInt(int64(mtp.Id), 10)), +func (k Keeper) EmitForceClose(ctx sdk.Context, trigger string, mtp types.MTP, repayAmount, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt math.Int, closer string, allInterestsPaid bool, tradingAssetPrice math.LegacyDec) { + ctx.EventManager().EmitEvent(sdk.NewEvent(types.EventForceClosed, + sdk.NewAttribute("mtp_id", strconv.FormatInt(int64(mtp.Id), 10)), + sdk.NewAttribute("owner", mtp.Address), + sdk.NewAttribute("amm_pool_id", strconv.FormatInt(int64(mtp.AmmPoolId), 10)), sdk.NewAttribute("position", mtp.Position.String()), - sdk.NewAttribute("address", mtp.Address), - sdk.NewAttribute("collaterals", mtp.Collateral.String()), - sdk.NewAttribute("custodies", mtp.Custody.String()), - sdk.NewAttribute("repay_amount", repayAmount.String()), - sdk.NewAttribute("liabilities", mtp.Liabilities.String()), - sdk.NewAttribute("borrow_interest_unpaid_liability", mtp.BorrowInterestUnpaidLiability.String()), - sdk.NewAttribute("borrow_interest_paid_custody", mtp.BorrowInterestPaidCustody.String()), - sdk.NewAttribute("health", mtp.MtpHealth.String()), + sdk.NewAttribute("collateral_asset", mtp.CollateralAsset), sdk.NewAttribute("closer", closer), - )) -} - -func (k Keeper) EmitFundingFeePayment(ctx sdk.Context, mtp *types.MTP, takeAmount math.Int, takeAsset string, paymentType string) { - ctx.EventManager().EmitEvent(sdk.NewEvent(paymentType, - sdk.NewAttribute("id", strconv.FormatInt(int64(mtp.Id), 10)), - sdk.NewAttribute("payment_amount", takeAmount.String()), - sdk.NewAttribute("payment_asset", takeAsset), + sdk.NewAttribute("repay_amount", repayAmount.String()), + sdk.NewAttribute("return_amount", returnAmt.String()), + sdk.NewAttribute("funding_fee_amount", fundingFeeAmt.String()), + sdk.NewAttribute("funding_amount_distributed", fundingAmtDistributed.String()), + sdk.NewAttribute("interest_amount", interestAmt.String()), + sdk.NewAttribute("insurance_amount", insuranceAmt.String()), + sdk.NewAttribute("funding_fee_paid_custody", mtp.FundingFeePaidCustody.String()), + sdk.NewAttribute("funding_fee_received_custody", mtp.FundingFeeReceivedCustody.String()), + sdk.NewAttribute("trading_asset_price", tradingAssetPrice.String()), + sdk.NewAttribute("all_interests_paid", strconv.FormatBool(allInterestsPaid)), // Funding Fee is fully paid but interest amount is only partially paid then this will be false + sdk.NewAttribute("trigger", trigger), )) } diff --git a/x/perpetual/keeper/force_close.go b/x/perpetual/keeper/force_close.go new file mode 100644 index 000000000..db6f1a6aa --- /dev/null +++ b/x/perpetual/keeper/force_close.go @@ -0,0 +1,32 @@ +package keeper + +import ( + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + ammtypes "github.com/elys-network/elys/x/amm/types" + "github.com/elys-network/elys/x/perpetual/types" +) + +func (k Keeper) ForceClose(ctx sdk.Context, mtp *types.MTP, pool *types.Pool, ammPool *ammtypes.Pool) (math.Int, math.Int, error) { + repayAmount := math.ZeroInt() + + // Estimate swap and repay + repayAmt, returnAmount, err := k.EstimateAndRepay(ctx, mtp, pool, ammPool, math.LegacyOneDec()) + if err != nil { + return math.ZeroInt(), math.ZeroInt(), err + } + + repayAmount = repayAmount.Add(repayAmt) + + address := sdk.MustAccAddressFromBech32(mtp.Address) + // EpochHooks after perpetual position closed + if k.hooks != nil { + params := k.GetParams(ctx) + err = k.hooks.AfterPerpetualPositionClosed(ctx, *ammPool, *pool, address, params.EnableTakeProfitCustodyLiabilities) + if err != nil { + return math.Int{}, math.Int{}, err + } + } + + return repayAmt, returnAmount, nil +} diff --git a/x/perpetual/keeper/force_close_long.go b/x/perpetual/keeper/force_close_long.go deleted file mode 100644 index 7debbb40f..000000000 --- a/x/perpetual/keeper/force_close_long.go +++ /dev/null @@ -1,36 +0,0 @@ -package keeper - -import ( - "cosmossdk.io/math" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/elys-network/elys/x/perpetual/types" -) - -func (k Keeper) ForceCloseLong(ctx sdk.Context, mtp *types.MTP, pool *types.Pool, takeFundPayment bool, baseCurrency string) (math.Int, error) { - repayAmount := math.ZeroInt() - // Retrieve AmmPool - ammPool, err := k.GetAmmPool(ctx, mtp.AmmPoolId) - if err != nil { - return math.ZeroInt(), err - } - - // Estimate swap and repay - repayAmt, err := k.EstimateAndRepay(ctx, mtp, pool, &ammPool, baseCurrency, math.LegacyOneDec()) - if err != nil { - return math.ZeroInt(), err - } - - repayAmount = repayAmount.Add(repayAmt) - - // EpochHooks after perpetual position closed - address := sdk.MustAccAddressFromBech32(mtp.Address) - if k.hooks != nil { - params := k.GetParams(ctx) - err = k.hooks.AfterPerpetualPositionClosed(ctx, ammPool, *pool, address, params.EnableTakeProfitCustodyLiabilities) - if err != nil { - return math.Int{}, err - } - } - - return repayAmount, nil -} diff --git a/x/perpetual/keeper/force_close_short.go b/x/perpetual/keeper/force_close_short.go deleted file mode 100644 index 9e861c25a..000000000 --- a/x/perpetual/keeper/force_close_short.go +++ /dev/null @@ -1,36 +0,0 @@ -package keeper - -import ( - "cosmossdk.io/math" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/elys-network/elys/x/perpetual/types" -) - -func (k Keeper) ForceCloseShort(ctx sdk.Context, mtp *types.MTP, pool *types.Pool, takeFundPayment bool, baseCurrency string) (math.Int, error) { - repayAmount := math.ZeroInt() - // Retrieve AmmPool - ammPool, err := k.GetAmmPool(ctx, mtp.AmmPoolId) - if err != nil { - return math.ZeroInt(), err - } - - // Estimate swap and repay - repayAmt, err := k.EstimateAndRepay(ctx, mtp, pool, &ammPool, baseCurrency, math.LegacyOneDec()) - if err != nil { - return math.ZeroInt(), err - } - - repayAmount = repayAmount.Add(repayAmt) - - address := sdk.MustAccAddressFromBech32(mtp.Address) - // EpochHooks after perpetual position closed - if k.hooks != nil { - params := k.GetParams(ctx) - err = k.hooks.AfterPerpetualPositionClosed(ctx, ammPool, *pool, address, params.EnableTakeProfitCustodyLiabilities) - if err != nil { - return math.Int{}, err - } - } - - return repayAmount, nil -} diff --git a/x/perpetual/keeper/force_close_short_test.go b/x/perpetual/keeper/force_close_test.go similarity index 83% rename from x/perpetual/keeper/force_close_short_test.go rename to x/perpetual/keeper/force_close_test.go index c11f54eb0..002387dd1 100644 --- a/x/perpetual/keeper/force_close_short_test.go +++ b/x/perpetual/keeper/force_close_test.go @@ -11,21 +11,6 @@ import ( "github.com/elys-network/elys/x/perpetual/types" ) -func (suite *PerpetualKeeperTestSuite) TestForceCloseShort_PoolNotFound() { - - ctx := suite.ctx - k := suite.app.PerpetualKeeper - mtp := &types.MTP{ - AmmPoolId: 8000, - } - - pool := &types.Pool{} - - _, err := k.ForceCloseShort(ctx, mtp, pool, false, ptypes.BaseCurrency) - - suite.Require().ErrorIs(err, types.ErrPoolDoesNotExist) -} - func (suite *PerpetualKeeperTestSuite) TestForceCloseShort_Successful() { ctx := suite.ctx @@ -73,7 +58,7 @@ func (suite *PerpetualKeeperTestSuite) TestForceCloseShort_Successful() { suite.Require().Nil(err) - _, err = k.ForceCloseShort(ctx, &mtp, &pool, false, ptypes.BaseCurrency) + _, _, err = k.ForceClose(ctx, &mtp, &pool, &ammPool) suite.Require().Nil(err) } diff --git a/x/perpetual/keeper/keeper.go b/x/perpetual/keeper/keeper.go index c163c67e9..b5ac6ef11 100644 --- a/x/perpetual/keeper/keeper.go +++ b/x/perpetual/keeper/keeper.go @@ -1,11 +1,9 @@ package keeper import ( - "fmt" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - "cosmossdk.io/core/store" "cosmossdk.io/log" + "fmt" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" @@ -287,14 +285,14 @@ func (k Keeper) BorrowInterestRateComputation(ctx sdk.Context, pool types.Pool) return newBorrowInterestRate, nil } -func (k Keeper) TakeFundPayment(ctx sdk.Context, amount math.Int, returnAsset string, ammPool *ammtypes.Pool) (math.Int, error) { +func (k Keeper) CollectInsuranceFund(ctx sdk.Context, amount math.Int, returnAsset string, ammPool *ammtypes.Pool) (math.Int, error) { params := k.GetParams(ctx) fundAddr := sdk.MustAccAddressFromBech32(params.BorrowInterestPaymentFundAddress) - takeAmount := amount.ToLegacyDec().Mul(params.BorrowInterestPaymentFundPercentage).TruncateInt() + insuranceAmount := amount.ToLegacyDec().Mul(params.BorrowInterestPaymentFundPercentage).TruncateInt() - if !takeAmount.IsZero() { - takeCoins := sdk.NewCoins(sdk.NewCoin(returnAsset, takeAmount)) + if !insuranceAmount.IsZero() { + takeCoins := sdk.NewCoins(sdk.NewCoin(returnAsset, insuranceAmount)) err := k.SendFromAmmPool(ctx, ammPool, fundAddr, takeCoins) if err != nil { @@ -302,23 +300,7 @@ func (k Keeper) TakeFundPayment(ctx sdk.Context, amount math.Int, returnAsset st } } - return takeAmount, nil -} - -func (k Keeper) TransferRevenueAmount(ctx sdk.Context, revenueAmt math.Int, returnAsset string, ammPool *ammtypes.Pool) error { - - if !revenueAmt.IsZero() { - revenueCoins := sdk.NewCoins(sdk.NewCoin(returnAsset, revenueAmt)) - - toAddress := authtypes.NewModuleAddress(types.ModuleName) - - err := k.SendFromAmmPool(ctx, ammPool, toAddress, revenueCoins) - if err != nil { - return err - } - - } - return nil + return insuranceAmount, nil } // Set the perpetual hooks. diff --git a/x/perpetual/keeper/msg_server_close_positions.go b/x/perpetual/keeper/msg_server_close_positions.go index ab12bb0cd..67f47d618 100644 --- a/x/perpetual/keeper/msg_server_close_positions.go +++ b/x/perpetual/keeper/msg_server_close_positions.go @@ -3,23 +3,14 @@ package keeper import ( "context" "fmt" - "strings" - sdk "github.com/cosmos/cosmos-sdk/types" - ptypes "github.com/elys-network/elys/x/parameter/types" "github.com/elys-network/elys/x/perpetual/types" ) func (k msgServer) ClosePositions(goCtx context.Context, msg *types.MsgClosePositions) (*types.MsgClosePositionsResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - baseCurrency, found := k.assetProfileKeeper.GetEntry(ctx, ptypes.BaseCurrency) - if !found { - return nil, nil - } - // Handle liquidations - liqLog := []string{} for _, val := range msg.Liquidate { owner := sdk.MustAccAddressFromBech32(val.Address) position, err := k.GetMTP(ctx, owner, val.Id) @@ -27,6 +18,7 @@ func (k msgServer) ClosePositions(goCtx context.Context, msg *types.MsgClosePosi continue } + // We fetch the amm pool again as there can be changes in amm pool when position is closed pool, poolFound := k.GetPool(ctx, position.AmmPoolId) if !poolFound { continue @@ -37,18 +29,16 @@ func (k msgServer) ClosePositions(goCtx context.Context, msg *types.MsgClosePosi } cachedCtx, write := ctx.CacheContext() - err = k.CheckAndLiquidateUnhealthyPosition(cachedCtx, &position, pool, ammPool, baseCurrency.Denom) + err = k.CheckAndLiquidatePosition(cachedCtx, &position, pool, &ammPool, msg.Creator) if err == nil { write() } if err != nil { - // Add log about error or not liquidated - liqLog = append(liqLog, fmt.Sprintf("Position: Address:%s Id:%d cannot be liquidated due to err: %s", position.Address, position.Id, err.Error())) + ctx.Logger().Error(fmt.Sprintf("MTP Unhealthy Close Position: Address:%s Id:%d cannot be liquidated due to err: %s", position.Address, position.Id, err.Error())) } } //Handle StopLoss - closeLog := []string{} for _, val := range msg.StopLoss { owner := sdk.MustAccAddressFromBech32(val.Address) position, err := k.GetMTP(ctx, owner, val.Id) @@ -61,19 +51,22 @@ func (k msgServer) ClosePositions(goCtx context.Context, msg *types.MsgClosePosi continue } + ammPool, poolErr := k.GetAmmPool(ctx, position.AmmPoolId) + if poolErr != nil { + continue + } + cachedCtx, write := ctx.CacheContext() - err = k.CheckAndCloseAtStopLoss(cachedCtx, &position, pool, baseCurrency.Denom) + err = k.CheckAndLiquidatePosition(cachedCtx, &position, pool, &ammPool, msg.Creator) if err == nil { write() } if err != nil { - // Add log about error or not closed - closeLog = append(closeLog, fmt.Sprintf("Position: Address:%s Id:%d cannot be liquidated due to err: %s", position.Address, position.Id, err.Error())) + ctx.Logger().Error(fmt.Sprintf("MTP StopLossPrice Close Position: Address:%s Id:%d cannot be liquidated due to err: %s", position.Address, position.Id, err.Error())) } } //Handle take profit - takeProfitLog := []string{} for _, val := range msg.TakeProfit { owner := sdk.MustAccAddressFromBech32(val.Address) position, err := k.GetMTP(ctx, owner, val.Id) @@ -86,22 +79,20 @@ func (k msgServer) ClosePositions(goCtx context.Context, msg *types.MsgClosePosi continue } + ammPool, poolErr := k.GetAmmPool(ctx, position.AmmPoolId) + if poolErr != nil { + continue + } + cachedCtx, write := ctx.CacheContext() - err = k.CheckAndCloseAtTakeProfit(cachedCtx, &position, pool, baseCurrency.Denom) + err = k.CheckAndLiquidatePosition(cachedCtx, &position, pool, &ammPool, msg.Creator) if err == nil { write() } if err != nil { - // Add log about error or not closed - takeProfitLog = append(takeProfitLog, fmt.Sprintf("Position: Address:%s Id:%d cannot be liquidated due to err: %s", position.Address, position.Id, err.Error())) + ctx.Logger().Error(fmt.Sprintf("MTP TakeProfitPrice Close Position: Address:%s Id:%d cannot be liquidated due to err: %s", position.Address, position.Id, err.Error())) } } - ctx.EventManager().EmitEvent(sdk.NewEvent(types.EventClosePositions, - sdk.NewAttribute("liquidations", strings.Join(liqLog, "\n")), - sdk.NewAttribute("stop_loss", strings.Join(closeLog, "\n")), - sdk.NewAttribute("take_profit", strings.Join(takeProfitLog, "\n")), - )) - return &types.MsgClosePositionsResponse{}, nil } diff --git a/x/perpetual/keeper/msg_server_update_stop_loss.go b/x/perpetual/keeper/msg_server_update_stop_loss.go index b2000c8e1..0036ad05c 100644 --- a/x/perpetual/keeper/msg_server_update_stop_loss.go +++ b/x/perpetual/keeper/msg_server_update_stop_loss.go @@ -21,16 +21,31 @@ func (k msgServer) UpdateStopLoss(goCtx context.Context, msg *types.MsgUpdateSto } poolId := mtp.AmmPoolId - _, found := k.GetPool(ctx, poolId) + pool, found := k.GetPool(ctx, poolId) if !found { return nil, errorsmod.Wrap(types.ErrPoolDoesNotExist, fmt.Sprintf("poolId: %d", poolId)) } + ammPool, err := k.GetAmmPool(ctx, pool.AmmPoolId) + if err != nil { + return nil, errorsmod.Wrap(err, "amm pool not found") + } + + repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, allInterestsPaid, forceClosed, err := k.MTPTriggerChecksAndUpdates(ctx, &mtp, &pool, &ammPool) + if err != nil { + return nil, err + } + tradingAssetPrice, err := k.GetAssetPrice(ctx, mtp.TradingAsset) if err != nil { return nil, err } + if forceClosed { + k.EmitForceClose(ctx, "update_stop_loss", mtp, repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, msg.Creator, allInterestsPaid, tradingAssetPrice) + return &types.MsgUpdateStopLossResponse{}, nil + } + if mtp.Position == types.Position_LONG { if !msg.Price.IsZero() && msg.Price.GTE(tradingAssetPrice) { return nil, fmt.Errorf("stop loss price cannot be greater than equal to tradingAssetPrice for long (Stop loss: %s, asset price: %s)", msg.Price.String(), tradingAssetPrice.String()) @@ -48,10 +63,15 @@ func (k msgServer) UpdateStopLoss(goCtx context.Context, msg *types.MsgUpdateSto return nil, err } - event := sdk.NewEvent(types.EventOpen, - sdk.NewAttribute("id", strconv.FormatInt(int64(mtp.Id), 10)), - sdk.NewAttribute("address", mtp.Address), + event := sdk.NewEvent(types.EventUpdateStopLoss, + sdk.NewAttribute("mtp_id", strconv.FormatInt(int64(mtp.Id), 10)), + sdk.NewAttribute("owner", mtp.Address), sdk.NewAttribute("stop_loss", mtp.StopLossPrice.String()), + sdk.NewAttribute("funding_fee_amount", fundingFeeAmt.String()), + sdk.NewAttribute("interest_amount", interestAmt.String()), + sdk.NewAttribute("insurance_amount", insuranceAmt.String()), + sdk.NewAttribute("funding_fee_paid_custody", mtp.FundingFeePaidCustody.String()), + sdk.NewAttribute("funding_fee_received_custody", mtp.FundingFeeReceivedCustody.String()), ) ctx.EventManager().EmitEvent(event) diff --git a/x/perpetual/keeper/msg_server_update_stop_loss_test.go b/x/perpetual/keeper/msg_server_update_stop_loss_test.go index 8abc04563..f00b35eee 100644 --- a/x/perpetual/keeper/msg_server_update_stop_loss_test.go +++ b/x/perpetual/keeper/msg_server_update_stop_loss_test.go @@ -88,7 +88,7 @@ func (suite *PerpetualKeeperTestSuite) TestUpdateStopLossPrice() { Price: math.LegacyNewDec(2), } }, - "asset price uatom not found", + "price for outToken not set: uatom", }, { "success: Stop Loss price updated", @@ -197,6 +197,51 @@ func (suite *PerpetualKeeperTestSuite) TestUpdateStopLossPrice() { }, "stop loss price cannot be less than equal to tradingAssetPrice for short", }, + { + "Force close when trying to update stop loss in unhealthy position", + func() *types.MsgUpdateStopLoss { + suite.ResetSuite() + + addr := suite.AddAccounts(1, nil) + positionCreator := addr[0] + _, _, ammPool := suite.SetPerpetualPool(1) + suite.app.OracleKeeper.SetPrice(suite.ctx, oracletypes.Price{ + Asset: "ATOM", + Price: math.LegacyMustNewDecFromStr("2"), + Source: "elys", + Provider: oracleProvider.String(), + Timestamp: uint64(suite.ctx.BlockTime().Unix()), + }) + + openPositionMsg := &types.MsgOpen{ + Creator: positionCreator.String(), + Leverage: math.LegacyNewDec(5), + Position: types.Position_LONG, + PoolId: ammPool.PoolId, + TradingAsset: ptypes.ATOM, + Collateral: sdk.NewCoin(ptypes.BaseCurrency, math.NewInt(1000)), + TakeProfitPrice: math.LegacyMustNewDecFromStr("2.9"), + StopLossPrice: math.LegacyZeroDec(), + } + position, err := suite.app.PerpetualKeeper.Open(suite.ctx, openPositionMsg) + suite.Require().NoError(err) + + suite.app.OracleKeeper.SetPrice(suite.ctx, oracletypes.Price{ + Asset: "ATOM", + Price: math.LegacyMustNewDecFromStr("0.00001"), + Source: "elys", + Provider: oracleProvider.String(), + Timestamp: uint64(suite.ctx.BlockTime().Unix()), + }) + + return &types.MsgUpdateStopLoss{ + Creator: positionCreator.String(), + Id: position.Id, + Price: math.LegacyMustNewDecFromStr("2"), + } + }, + "", + }, } for _, tc := range testCases { diff --git a/x/perpetual/keeper/msg_server_update_take_profit_price.go b/x/perpetual/keeper/msg_server_update_take_profit_price.go index d1c1ff8d8..e3bc2ab39 100644 --- a/x/perpetual/keeper/msg_server_update_take_profit_price.go +++ b/x/perpetual/keeper/msg_server_update_take_profit_price.go @@ -26,13 +26,27 @@ func (k msgServer) UpdateTakeProfitPrice(goCtx context.Context, msg *types.MsgUp return nil, errorsmod.Wrap(types.ErrPoolDoesNotExist, fmt.Sprintf("poolId: %d", poolId)) } - params := k.GetParams(ctx) + ammPool, err := k.GetAmmPool(ctx, pool.AmmPoolId) + if err != nil { + return nil, errorsmod.Wrap(err, "amm pool not found") + } + + repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, allInterestsPaid, forceClosed, err := k.MTPTriggerChecksAndUpdates(ctx, &mtp, &pool, &ammPool) + if err != nil { + return nil, err + } tradingAssetPrice, err := k.GetAssetPrice(ctx, mtp.TradingAsset) if err != nil { return nil, err } + if forceClosed { + k.EmitForceClose(ctx, "update_take_profit", mtp, repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, msg.Creator, allInterestsPaid, tradingAssetPrice) + return &types.MsgUpdateTakeProfitPriceResponse{}, nil + } + + params := k.GetParams(ctx) ratio := msg.Price.Quo(tradingAssetPrice) if mtp.Position == types.Position_LONG { if ratio.LT(params.MinimumLongTakeProfitPriceRatio) || ratio.GT(params.MaximumLongTakeProfitPriceRatio) { @@ -71,23 +85,22 @@ func (k msgServer) UpdateTakeProfitPrice(goCtx context.Context, msg *types.MsgUp k.SetPool(ctx, pool) - ammPool, err := k.GetAmmPool(ctx, poolId) - if err != nil { - return nil, err - } - if k.hooks != nil { - params := k.GetParams(ctx) err = k.hooks.AfterPerpetualPositionModified(ctx, ammPool, pool, creator, params.EnableTakeProfitCustodyLiabilities) if err != nil { return nil, err } } - event := sdk.NewEvent(types.EventOpen, - sdk.NewAttribute("id", strconv.FormatInt(int64(mtp.Id), 10)), - sdk.NewAttribute("address", mtp.Address), + event := sdk.NewEvent(types.EventUpdateTakeProfitPrice, + sdk.NewAttribute("mtp_id", strconv.FormatInt(int64(mtp.Id), 10)), + sdk.NewAttribute("owner", mtp.Address), sdk.NewAttribute("take_profit_price", mtp.TakeProfitPrice.String()), + sdk.NewAttribute("funding_fee_amount", fundingFeeAmt.String()), + sdk.NewAttribute("interest_amount", interestAmt.String()), + sdk.NewAttribute("insurance_amount", insuranceAmt.String()), + sdk.NewAttribute("funding_fee_paid_custody", mtp.FundingFeePaidCustody.String()), + sdk.NewAttribute("funding_fee_received_custody", mtp.FundingFeeReceivedCustody.String()), ) ctx.EventManager().EmitEvent(event) diff --git a/x/perpetual/keeper/msg_server_update_take_profit_price_test.go b/x/perpetual/keeper/msg_server_update_take_profit_price_test.go index 428d63f85..779fe1716 100644 --- a/x/perpetual/keeper/msg_server_update_take_profit_price_test.go +++ b/x/perpetual/keeper/msg_server_update_take_profit_price_test.go @@ -91,7 +91,7 @@ func (suite *PerpetualKeeperTestSuite) TestUpdateTakeProfitPrice() { Price: math.LegacyNewDec(2), } }, - "asset price uatom not found", + "price for outToken not set: uatom", }, { "success: take profit price updated", diff --git a/x/perpetual/keeper/mtp.go b/x/perpetual/keeper/mtp.go index 4679532e6..26ac04d5f 100644 --- a/x/perpetual/keeper/mtp.go +++ b/x/perpetual/keeper/mtp.go @@ -1,6 +1,7 @@ package keeper import ( + "errors" "fmt" "cosmossdk.io/math" @@ -30,6 +31,7 @@ func (k Keeper) SetMTP(ctx sdk.Context, mtp *types.MTP) error { k.SetOpenMTPCount(ctx, openCount) } + // TODO Do we need validate MTP every single time we set it? if err := mtp.Validate(); err != nil { return err } @@ -161,7 +163,7 @@ func (k Keeper) fillMTPData(ctx sdk.Context, mtp types.MTP, baseCurrency string) // Update interest first and then calculate health k.UpdateMTPBorrowInterestUnpaidLiability(ctx, &mtp) - err := k.UpdateFundingFee(ctx, &mtp, &pool, ammPool) + _, _, _, err := k.UpdateFundingFee(ctx, &mtp, &pool) if err != nil { return nil, err } @@ -305,7 +307,7 @@ func (k Keeper) GetEstimatedPnL(ctx sdk.Context, mtp types.MTP, baseCurrency str tradingAssetPrice = mtp.TakeProfitPrice } if tradingAssetPrice.IsZero() { - return math.Int{}, fmt.Errorf("trading asset price is zero") + return math.Int{}, errors.New("trading asset price is zero") } // in long it's in trading asset ,if short position, custody asset is already in base currency diff --git a/x/perpetual/keeper/mtp_borrow_interest.go b/x/perpetual/keeper/mtp_borrow_interest.go index 05166b162..7f86299d3 100644 --- a/x/perpetual/keeper/mtp_borrow_interest.go +++ b/x/perpetual/keeper/mtp_borrow_interest.go @@ -29,33 +29,29 @@ func (k Keeper) UpdateMTPBorrowInterestUnpaidLiability(ctx sdk.Context, mtp *typ } // SettleMTPBorrowInterestUnpaidLiability This does not update BorrowInterestUnpaidLiability, it should be done through UpdateMTPBorrowInterestUnpaidLiability beforehand -func (k Keeper) SettleMTPBorrowInterestUnpaidLiability(ctx sdk.Context, mtp *types.MTP, pool *types.Pool, ammPool ammtypes.Pool) (math.Int, error) { +func (k Keeper) SettleMTPBorrowInterestUnpaidLiability(ctx sdk.Context, mtp *types.MTP, pool *types.Pool, ammPool *ammtypes.Pool) (math.Int, math.Int, bool, error) { // adding case for mtp.BorrowInterestUnpaidLiability being smaller tha 10^-18, this happens when position is small so liabilities are small, and hardly any time has spend, so interests are small, so it leads to 0 value if mtp.BorrowInterestUnpaidLiability.IsZero() { // nothing to pay back - return math.ZeroInt(), nil + return math.ZeroInt(), math.ZeroInt(), true, nil } tradingAssetPrice, err := k.GetAssetPrice(ctx, mtp.TradingAsset) if err != nil { - return math.ZeroInt(), err + return math.ZeroInt(), math.ZeroInt(), true, err } borrowInterestPaymentInCustody, err := mtp.GetBorrowInterestAmountAsCustodyAsset(tradingAssetPrice) if err != nil { - return math.ZeroInt(), err + return math.ZeroInt(), math.ZeroInt(), true, err } // here we are paying the interests so unpaid borrow interest reset to 0 mtp.BorrowInterestUnpaidLiability = math.ZeroInt() - // edge case, not enough custody to cover payment - // TODO This should not happen, bot should close the position beforehand - // TODO what if bot misses it, can we do anything about it? + fullyPaid := true if borrowInterestPaymentInCustody.GT(mtp.Custody) { - // TODO Do we need to keep this swap? as health will already be 0 as custody will be 0 - // TODO Doing this swap to update back mtp.BorrowInterestUnpaidLiability again as there aren't enough custody unpaidInterestCustody := borrowInterestPaymentInCustody.Sub(mtp.Custody) unpaidInterestLiabilities := math.ZeroInt() if mtp.Position == types.Position_LONG { @@ -69,6 +65,7 @@ func (k Keeper) SettleMTPBorrowInterestUnpaidLiability(ctx sdk.Context, mtp *typ // Since not enough custody left, we can only pay this much, position health goes to 0 borrowInterestPaymentInCustody = mtp.Custody + fullyPaid = false } mtp.BorrowInterestPaidCustody = mtp.BorrowInterestPaidCustody.Add(borrowInterestPaymentInCustody) @@ -77,20 +74,20 @@ func (k Keeper) SettleMTPBorrowInterestUnpaidLiability(ctx sdk.Context, mtp *typ // This will go to zero if borrowInterestPaymentInCustody.GT(mtp.Custody) true mtp.Custody = mtp.Custody.Sub(borrowInterestPaymentInCustody) - takeAmount, err := k.TakeFundPayment(ctx, borrowInterestPaymentInCustody, mtp.CustodyAsset, &ammPool) - if err != nil { - return math.ZeroInt(), err - } - - if !takeAmount.IsZero() { - k.EmitFundPayment(ctx, mtp, takeAmount, mtp.CustodyAsset, types.EventIncrementalPayFund) + insuranceAmount := math.ZeroInt() + // if full interest is paid then only collect insurance fund + if fullyPaid { + insuranceAmount, err = k.CollectInsuranceFund(ctx, borrowInterestPaymentInCustody, mtp.CustodyAsset, ammPool) + if err != nil { + return math.ZeroInt(), math.ZeroInt(), fullyPaid, err + } } err = pool.UpdateCustody(mtp.CustodyAsset, borrowInterestPaymentInCustody, false, mtp.Position) if err != nil { - return math.ZeroInt(), err + return math.ZeroInt(), math.ZeroInt(), fullyPaid, err } - return borrowInterestPaymentInCustody, nil + return borrowInterestPaymentInCustody, insuranceAmount, fullyPaid, nil } diff --git a/x/perpetual/keeper/mtp_health.go b/x/perpetual/keeper/mtp_health.go index 93f9cf8dc..54fa55a9a 100644 --- a/x/perpetual/keeper/mtp_health.go +++ b/x/perpetual/keeper/mtp_health.go @@ -10,11 +10,17 @@ import ( // GetMTPHealth Health = custody / liabilities // It's responsibility of outer function to update mtp.BorrowInterestUnpaidLiability using UpdateMTPBorrowInterestUnpaidLiability func (k Keeper) GetMTPHealth(ctx sdk.Context, mtp types.MTP, ammPool ammtypes.Pool, baseCurrency string) (math.LegacyDec, error) { + + if mtp.Custody.LTE(math.ZeroInt()) { + return math.LegacyZeroDec(), nil + } + if mtp.Liabilities.IsZero() { return math.LegacyMaxSortableDec, nil } // For long this unit is base currency, for short this is in trading asset + // We do not consider here funding fee because it has been / should be already subtracted from mtp.Custody, the custody amt can be <= 0, then above it returns 0 totalLiabilities := mtp.Liabilities.Add(mtp.BorrowInterestUnpaidLiability) // if short position, convert liabilities to base currency diff --git a/x/perpetual/keeper/mtp_trigger.go b/x/perpetual/keeper/mtp_trigger.go new file mode 100644 index 000000000..a56a96879 --- /dev/null +++ b/x/perpetual/keeper/mtp_trigger.go @@ -0,0 +1,93 @@ +package keeper + +import ( + sdkerrors "cosmossdk.io/errors" + "cosmossdk.io/math" + "errors" + sdk "github.com/cosmos/cosmos-sdk/types" + ammtypes "github.com/elys-network/elys/x/amm/types" + ptypes "github.com/elys-network/elys/x/parameter/types" + "github.com/elys-network/elys/x/perpetual/types" +) + +// MTPTriggerChecksAndUpdates Should be run whenever there is a state change of MTP. Runs in following order: +// 1. Settle funding fee, if unable to settle whole, close the position +// 2. Settle interest payments, in unable to pay whole, close the position +// 3. Update position health and check if above minimum threshold, if equal or lower close the position +func (k Keeper) MTPTriggerChecksAndUpdates(ctx sdk.Context, mtp *types.MTP, pool *types.Pool, ammPool *ammtypes.Pool) (repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt math.Int, allInterestsPaid, forceClosed bool, err error) { + + allInterestsPaid = true + forceClosed = false + fundingFeeFullyPaid := true + interestFullyPaid := true + + // Update interests + k.UpdateMTPBorrowInterestUnpaidLiability(ctx, mtp) + + // Pay funding fee + fundingFeeFullyPaid, fundingFeeAmt, fundingAmtDistributed, err = k.SettleFunding(ctx, mtp, pool) + if err != nil { + return repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, allInterestsPaid, forceClosed, sdkerrors.Wrap(err, "error handling funding fee") + } + + // Unable to pay funding fee, close the position + if !fundingFeeFullyPaid { + allInterestsPaid = false + forceClosed = true + repayAmt, returnAmt, err = k.ForceClose(ctx, mtp, pool, ammPool) + if err != nil { + return repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, allInterestsPaid, forceClosed, sdkerrors.Wrap(err, "error executing force close") + } + + return repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, math.ZeroInt(), math.ZeroInt(), allInterestsPaid, forceClosed, nil + } + + // Pay interests + interestAmt, insuranceAmt, interestFullyPaid, err = k.SettleMTPBorrowInterestUnpaidLiability(ctx, mtp, pool, ammPool) + if err != nil { + return repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, allInterestsPaid, forceClosed, sdkerrors.Wrap(err, "error handling borrow interest payment") + } + + // Unable to pay interests, close the position + if !interestFullyPaid { + allInterestsPaid = false + forceClosed = true + repayAmt, returnAmt, err = k.ForceClose(ctx, mtp, pool, ammPool) + if err != nil { + return repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, allInterestsPaid, forceClosed, sdkerrors.Wrap(err, "error executing force close") + } + return repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, allInterestsPaid, forceClosed, nil + } + + baseCurrencyEntry, found := k.assetProfileKeeper.GetEntry(ctx, ptypes.BaseCurrency) + if !found { + return repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, allInterestsPaid, forceClosed, errors.New("unable to find base currency entry") + } + + baseCurrency := baseCurrencyEntry.Denom + + h, err := k.GetMTPHealth(ctx, *mtp, *ammPool, baseCurrency) + if err != nil { + return repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, allInterestsPaid, forceClosed, sdkerrors.Wrap(err, "error updating mtp health") + } + mtp.MtpHealth = h + + err = k.SetMTP(ctx, mtp) + if err != nil { + return repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, allInterestsPaid, forceClosed, err + } + + k.SetPool(ctx, *pool) + + safetyFactor := k.GetSafetyFactor(ctx) + // Position is unhealthy, close the position + if mtp.MtpHealth.LTE(safetyFactor) { + forceClosed = true + repayAmt, returnAmt, err = k.ForceClose(ctx, mtp, pool, ammPool) + if err != nil { + return repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, allInterestsPaid, forceClosed, sdkerrors.Wrap(err, "error executing force close") + } + } + + return repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, allInterestsPaid, forceClosed, nil +} diff --git a/x/perpetual/keeper/mtp_trigger_test.go b/x/perpetual/keeper/mtp_trigger_test.go new file mode 100644 index 000000000..dbb5b185c --- /dev/null +++ b/x/perpetual/keeper/mtp_trigger_test.go @@ -0,0 +1,158 @@ +package keeper_test + +import ( + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + ammtypes "github.com/elys-network/elys/x/amm/types" + ptypes "github.com/elys-network/elys/x/parameter/types" + "github.com/elys-network/elys/x/perpetual/types" + "time" +) + +func (suite *PerpetualKeeperTestSuite) resetForMTPTriggerChecksAndUpdates() (types.MTP, types.Pool, ammtypes.Pool, sdk.AccAddress) { + suite.ResetSuite() + addr := suite.AddAccounts(1, nil) + positionCreator := addr[0] + pool, _, ammPool := suite.SetPerpetualPool(1) + tradingAssetPrice, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) + suite.Require().NoError(err) + openPositionMsg := &types.MsgOpen{ + Creator: positionCreator.String(), + Leverage: math.LegacyNewDec(2), + Position: types.Position_LONG, + PoolId: ammPool.PoolId, + TradingAsset: ptypes.ATOM, + Collateral: sdk.NewCoin(ptypes.BaseCurrency, math.NewInt(1000_000)), + TakeProfitPrice: tradingAssetPrice.MulInt64(4), + StopLossPrice: math.LegacyZeroDec(), + } + + openPositionMsg2 := &types.MsgOpen{ + Creator: positionCreator.String(), + Leverage: math.LegacyNewDec(2), + Position: types.Position_SHORT, + PoolId: ammPool.PoolId, + TradingAsset: ptypes.ATOM, + Collateral: sdk.NewCoin(ptypes.BaseCurrency, math.NewInt(1000_000)), + TakeProfitPrice: tradingAssetPrice.QuoInt64(4), + StopLossPrice: math.LegacyZeroDec(), + } + + mtpOpenResponse, err := suite.app.PerpetualKeeper.Open(suite.ctx, openPositionMsg) + suite.Require().NoError(err) + _, err = suite.app.PerpetualKeeper.Open(suite.ctx, openPositionMsg2) + suite.Require().NoError(err) + mtp, err := suite.app.PerpetualKeeper.GetMTP(suite.ctx, positionCreator, mtpOpenResponse.Id) + suite.Require().NoError(err) + pool, _ = suite.app.PerpetualKeeper.GetPool(suite.ctx, mtp.Id) + ammPool, _ = suite.app.PerpetualKeeper.GetAmmPool(suite.ctx, mtp.AmmPoolId) + return mtp, pool, ammPool, addr[0] +} + +func (suite *PerpetualKeeperTestSuite) TestMTPTriggerChecksAndUpdates() { + mtp, pool, ammPool, _ := suite.resetForMTPTriggerChecksAndUpdates() + // Define test cases + testCases := []struct { + name string + setup func() + expectedErrMsg string + repayAmount math.Int + }{ + //{ + // "asset profile not found", + // func() { + // suite.app.AssetprofileKeeper.RemoveEntry(suite.ctx, ptypes.BaseCurrency) + // }, + // "unable to find base currency entry", + // math.NewInt(0), + //}, + { + "force close fails when unable to pay funding fee", + func() { + mtp.LastFundingCalcBlock = 1 + mtp.LastFundingCalcTime = 1 + suite.ctx = suite.ctx.WithBlockHeight(1).WithBlockTime(time.Now()) + suite.app.PerpetualKeeper.SetFundingRate(suite.ctx, 1, 1, types.FundingRateBlock{ + FundingRateLong: math.LegacyNewDec(10000), + FundingRateShort: math.LegacyNewDec(10000), + FundingAmountShort: math.LegacyNewDec(10000), + FundingAmountLong: math.LegacyNewDec(10000), + BlockHeight: 1, + BlockTime: 1, + }) + pool.FundingRate = math.LegacyNewDec(1000_000) + pool.BorrowInterestRate = math.LegacyNewDec(1) + suite.app.PerpetualKeeper.SetPool(suite.ctx, pool) + suite.app.AssetprofileKeeper.RemoveEntry(suite.ctx, ptypes.BaseCurrency) + }, + "error handling funding fee", + math.NewInt(0), + }, + // TODO need to be fixed when funding fee distribution is fixed + //{ + // "Success: force close when unable to pay funding fee", + // func() { + // mtp, pool, ammPool, _ = suite.resetForMTPTriggerChecksAndUpdates() + // mtp.LastFundingCalcBlock = 1 + // mtp.LastFundingCalcTime = 1 + // suite.ctx = suite.ctx.WithBlockHeight(1).WithBlockTime(time.Now()) + // suite.app.PerpetualKeeper.SetFundingRate(suite.ctx, 1, 1, types.FundingRateBlock{ + // FundingRateLong: math.LegacyNewDec(10000), + // FundingRateShort: math.LegacyNewDec(10000), + // FundingAmountShort: math.LegacyNewDec(10000), + // FundingAmountLong: math.LegacyNewDec(10000), + // BlockHeight: 1, + // BlockTime: 1, + // }) + // pool.FundingRate = math.LegacyNewDec(1000_000) + // pool.BorrowInterestRate = math.LegacyNewDec(1) + // suite.app.PerpetualKeeper.SetPool(suite.ctx, pool) + // }, + // "", + // math.NewInt(0), + //}, + { + "paying interest fail", + func() { + mtp, pool, ammPool, _ = suite.resetForMTPTriggerChecksAndUpdates() + mtp.LastInterestCalcBlock = 1 + mtp.LastInterestCalcTime = 1 + mtp.LastFundingCalcBlock = 1 + mtp.LastFundingCalcTime = 1 + suite.ctx = suite.ctx.WithBlockHeight(1).WithBlockTime(time.Now()) + suite.app.PerpetualKeeper.SetFundingRate(suite.ctx, 1, 1, types.FundingRateBlock{ + FundingRateLong: math.LegacyNewDec(0), + FundingRateShort: math.LegacyNewDec(0), + FundingAmountShort: math.LegacyNewDec(0), + FundingAmountLong: math.LegacyNewDec(0), + BlockHeight: 1, + BlockTime: 1, + }) + suite.app.PerpetualKeeper.SetBorrowRate(suite.ctx, 1, 1, types.InterestBlock{ + InterestRate: math.LegacyNewDec(1000_000), + BlockHeight: 1, + BlockTime: 1, + }) + pool.FundingRate = math.LegacyNewDec(0) + pool.BorrowInterestRate = math.LegacyNewDec(1000_000) + suite.app.PerpetualKeeper.SetPool(suite.ctx, pool) + }, + "", + math.NewInt(0), + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + tc.setup() + _, _, _, _, _, _, _, _, err := suite.app.PerpetualKeeper.MTPTriggerChecksAndUpdates(suite.ctx, &mtp, &pool, &ammPool) + + if tc.expectedErrMsg != "" { + suite.Require().Error(err) + suite.Require().Contains(err.Error(), tc.expectedErrMsg) + } else { + suite.Require().NoError(err) + } + }) + } +} diff --git a/x/perpetual/keeper/open.go b/x/perpetual/keeper/open.go index 5e7cdcda8..aa5b65a19 100644 --- a/x/perpetual/keeper/open.go +++ b/x/perpetual/keeper/open.go @@ -1,7 +1,9 @@ package keeper import ( + "errors" "fmt" + "strconv" errorsmod "cosmossdk.io/errors" "cosmossdk.io/math" @@ -66,7 +68,7 @@ func (k Keeper) Open(ctx sdk.Context, msg *types.MsgOpen) (*types.MsgOpenRespons if existingMtp == nil { // opening new position if msg.Leverage.LTE(math.LegacyOneDec()) { - return nil, fmt.Errorf("cannot open new position with leverage <= 1") + return nil, errors.New("cannot open new position with leverage <= 1") } // Check if max positions are exceeded as we are opening new position, not updating old position if err = k.CheckMaxOpenPositions(ctx); err != nil { @@ -117,8 +119,6 @@ func (k Keeper) Open(ctx sdk.Context, msg *types.MsgOpen) (*types.MsgOpenRespons return nil, err } - k.EmitOpenEvent(ctx, mtp) - creator := sdk.MustAccAddressFromBech32(msg.Creator) if k.hooks != nil { // pool values has been updated @@ -133,6 +133,24 @@ func (k Keeper) Open(ctx sdk.Context, msg *types.MsgOpen) (*types.MsgOpenRespons } } + ctx.EventManager().EmitEvent(sdk.NewEvent(types.EventOpen, + sdk.NewAttribute("mtp_id", strconv.FormatInt(int64(mtp.Id), 10)), + sdk.NewAttribute("owner", mtp.Address), + sdk.NewAttribute("position", mtp.Position.String()), + sdk.NewAttribute("amm_pool_id", strconv.FormatInt(int64(mtp.AmmPoolId), 10)), + sdk.NewAttribute("collateral_asset", mtp.CollateralAsset), + sdk.NewAttribute("collateral", mtp.Collateral.String()), + sdk.NewAttribute("liabilities", mtp.Liabilities.String()), + sdk.NewAttribute("custody", mtp.Custody.String()), + sdk.NewAttribute("mtp_health", mtp.MtpHealth.String()), + sdk.NewAttribute("stop_loss_price", mtp.StopLossPrice.String()), + sdk.NewAttribute("take_profit_price", mtp.TakeProfitPrice.String()), + sdk.NewAttribute("take_profit_borrow_factor", mtp.TakeProfitBorrowFactor.String()), + sdk.NewAttribute("funding_fee_paid_custody", mtp.FundingFeePaidCustody.String()), + sdk.NewAttribute("funding_fee_received_custody", mtp.FundingFeeReceivedCustody.String()), + sdk.NewAttribute("open_price", mtp.OpenPrice.String()), + )) + return &types.MsgOpenResponse{ Id: mtp.Id, }, nil diff --git a/x/perpetual/keeper/open_consolidate.go b/x/perpetual/keeper/open_consolidate.go index 93eeab8d7..80141a71e 100644 --- a/x/perpetual/keeper/open_consolidate.go +++ b/x/perpetual/keeper/open_consolidate.go @@ -2,6 +2,7 @@ package keeper import ( "fmt" + "strconv" errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" @@ -15,16 +16,25 @@ func (k Keeper) OpenConsolidate(ctx sdk.Context, existingMtp *types.MTP, newMtp return nil, err } - k.UpdateMTPBorrowInterestUnpaidLiability(ctx, existingMtp) - pool, found := k.GetPool(ctx, poolId) if !found { return nil, errorsmod.Wrap(types.ErrPoolDoesNotExist, fmt.Sprintf("poolId: %d", poolId)) } - err = k.SettleFunding(ctx, existingMtp, &pool, ammPool) + repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, allInterestsPaid, forceClosed, err := k.MTPTriggerChecksAndUpdates(ctx, existingMtp, &pool, &ammPool) if err != nil { - return nil, errorsmod.Wrapf(err, "error handling funding fee") + return nil, err + } + + if forceClosed { + tradingAssetPrice, err := k.GetAssetPrice(ctx, msg.TradingAsset) + if err != nil { + return nil, err + } + k.EmitForceClose(ctx, "open_consolidate", *existingMtp, repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, msg.Creator, allInterestsPaid, tradingAssetPrice) + return &types.MsgOpenResponse{ + Id: existingMtp.Id, + }, nil } existingMtp, err = k.OpenConsolidateMergeMtp(ctx, existingMtp, newMtp) @@ -67,8 +77,6 @@ func (k Keeper) OpenConsolidate(ctx sdk.Context, existingMtp *types.MTP, newMtp return nil, err } - k.EmitOpenEvent(ctx, existingMtp) - creator := sdk.MustAccAddressFromBech32(msg.Creator) if k.hooks != nil { params := k.GetParams(ctx) @@ -83,11 +91,25 @@ func (k Keeper) OpenConsolidate(ctx sdk.Context, existingMtp *types.MTP, newMtp return nil, err } + ctx.EventManager().EmitEvent(sdk.NewEvent(types.EventOpenConsolidate, + sdk.NewAttribute("mtp_id", strconv.FormatInt(int64(existingMtp.Id), 10)), + sdk.NewAttribute("owner", existingMtp.Address), + sdk.NewAttribute("position", existingMtp.Position.String()), + sdk.NewAttribute("amm_pool_id", strconv.FormatInt(int64(existingMtp.AmmPoolId), 10)), + sdk.NewAttribute("collateral_asset", existingMtp.CollateralAsset), + sdk.NewAttribute("collateral", existingMtp.Collateral.String()), + sdk.NewAttribute("liabilities", existingMtp.Liabilities.String()), + sdk.NewAttribute("custody", existingMtp.Custody.String()), + sdk.NewAttribute("mtp_health", existingMtp.MtpHealth.String()), + sdk.NewAttribute("stop_loss_price", existingMtp.StopLossPrice.String()), + sdk.NewAttribute("take_profit_price", existingMtp.TakeProfitPrice.String()), + sdk.NewAttribute("take_profit_borrow_factor", existingMtp.TakeProfitBorrowFactor.String()), + sdk.NewAttribute("funding_fee_paid_custody", existingMtp.FundingFeePaidCustody.String()), + sdk.NewAttribute("funding_fee_received_custody", existingMtp.FundingFeeReceivedCustody.String()), + sdk.NewAttribute("open_price", existingMtp.OpenPrice.String()), + )) + return &types.MsgOpenResponse{ Id: existingMtp.Id, }, nil } - -func (k Keeper) EmitOpenEvent(ctx sdk.Context, mtp *types.MTP) { - ctx.EventManager().EmitEvent(types.GenerateOpenEvent(mtp)) -} diff --git a/x/perpetual/keeper/open_consolidate_test.go b/x/perpetual/keeper/open_consolidate_test.go index fa8a01f25..12b8965e3 100644 --- a/x/perpetual/keeper/open_consolidate_test.go +++ b/x/perpetual/keeper/open_consolidate_test.go @@ -48,7 +48,7 @@ func (suite *PerpetualKeeperTestSuite) TestOpenConsolidate() { "perpetual pool does not exist", }, { - "Mtp health will be low for the safety factor", + "Force Closed: Mtp health will be low for the safety factor", func() (*types.MsgOpen, *types.MTP, *types.MTP) { suite.ResetSuite() @@ -81,7 +81,7 @@ func (suite *PerpetualKeeperTestSuite) TestOpenConsolidate() { return openPositionMsg, &mtp, &mtp }, - "mtp health would be too low for safety factor", + "", }, { "Sucess: MTP consolidation", diff --git a/x/perpetual/keeper/process_mtp.go b/x/perpetual/keeper/process_mtp.go index 4c43c5b89..ca8c39f4a 100644 --- a/x/perpetual/keeper/process_mtp.go +++ b/x/perpetual/keeper/process_mtp.go @@ -1,84 +1,14 @@ package keeper import ( - "fmt" - - "cosmossdk.io/errors" - "cosmossdk.io/math" - + sdkerrors "cosmossdk.io/errors" + "errors" sdk "github.com/cosmos/cosmos-sdk/types" - ammtypes "github.com/elys-network/elys/x/amm/types" "github.com/elys-network/elys/x/perpetual/types" ) -func (k Keeper) CheckAndLiquidateUnhealthyPosition(ctx sdk.Context, mtp *types.MTP, pool types.Pool, ammPool ammtypes.Pool, baseCurrency string) error { - var err error - - // update mtp take profit liabilities - // calculate mtp take profit liabilities, delta x_tp_l = delta y_tp_c * current price (take profit liabilities = take profit custody * current price) - mtp.TakeProfitLiabilities, err = k.CalcMTPTakeProfitLiability(ctx, *mtp) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("error calculating mtp take profit liabilities: %s", mtp.String())) - } - // calculate and update take profit borrow rate - err = mtp.UpdateMTPTakeProfitBorrowFactor() - if err != nil { - return errors.Wrap(err, fmt.Sprintf("error calculating mtp take profit borrow rate: %s", mtp.String())) - } - - k.UpdateMTPBorrowInterestUnpaidLiability(ctx, mtp) - // Handle Borrow Interest if within epoch position - if _, err := k.SettleMTPBorrowInterestUnpaidLiability(ctx, mtp, &pool, ammPool); err != nil { - return errors.Wrap(err, fmt.Sprintf("error handling borrow interest payment: %s", mtp.CollateralAsset)) - } - - err = k.SettleFunding(ctx, mtp, &pool, ammPool) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("error handling funding fee: %s", mtp.CollateralAsset)) - } - - h, err := k.GetMTPHealth(ctx, *mtp, ammPool, baseCurrency) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("error updating mtp health: %s", mtp.String())) - } - mtp.MtpHealth = h - - err = k.SetMTP(ctx, mtp) - if err != nil { - return err - } - - k.SetPool(ctx, pool) - - // check MTP health against threshold - safetyFactor := k.GetSafetyFactor(ctx) - - if mtp.MtpHealth.LTE(safetyFactor) { - var repayAmount math.Int - switch mtp.Position { - case types.Position_LONG: - repayAmount, err = k.ForceCloseLong(ctx, mtp, &pool, true, baseCurrency) - case types.Position_SHORT: - repayAmount, err = k.ForceCloseShort(ctx, mtp, &pool, true, baseCurrency) - default: - return errors.Wrap(types.ErrInvalidPosition, fmt.Sprintf("invalid position type: %s", mtp.Position)) - } - - if err == nil { - // Emit event if position was closed - k.EmitForceClose(ctx, types.EventForceCloseUnhealthy, mtp, repayAmount, "") - } else { - return errors.Wrap(err, "error executing force close") - } - } else { - ctx.Logger().Debug(errors.Wrap(types.ErrMTPHealthy, "skipping executing force close because mtp is healthy").Error()) - } - - return nil -} - -func (k Keeper) CheckAndCloseAtStopLoss(ctx sdk.Context, mtp *types.MTP, pool types.Pool, baseCurrency string) error { +func (k Keeper) CheckAndLiquidatePosition(ctx sdk.Context, mtp *types.MTP, pool types.Pool, ammPool *ammtypes.Pool, closer string) error { defer func() { if r := recover(); r != nil { if msg, ok := r.(string); ok { @@ -87,83 +17,38 @@ func (k Keeper) CheckAndCloseAtStopLoss(ctx sdk.Context, mtp *types.MTP, pool ty } }() - tradingAssetPrice, err := k.GetAssetPrice(ctx, mtp.TradingAsset) + repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, allInterestsPaid, forceClosed, err := k.MTPTriggerChecksAndUpdates(ctx, mtp, &pool, ammPool) if err != nil { return err } - if mtp.Position == types.Position_LONG { - underStopLossPrice := !mtp.StopLossPrice.IsNil() && tradingAssetPrice.LTE(mtp.StopLossPrice) - if !underStopLossPrice { - return fmt.Errorf("mtp stop loss price is not <= token price") - } - } else { - underStopLossPrice := !mtp.StopLossPrice.IsNil() && tradingAssetPrice.GTE(mtp.StopLossPrice) - if !underStopLossPrice { - return fmt.Errorf("mtp stop loss price is not => token price") - } - } - - var repayAmount math.Int - switch mtp.Position { - case types.Position_LONG: - repayAmount, err = k.ForceCloseLong(ctx, mtp, &pool, true, baseCurrency) - case types.Position_SHORT: - repayAmount, err = k.ForceCloseShort(ctx, mtp, &pool, true, baseCurrency) - default: - return errors.Wrap(types.ErrInvalidPosition, fmt.Sprintf("invalid position type: %s", mtp.Position)) - } - - if err == nil { - // Emit event if position was closed - k.EmitForceClose(ctx, types.EventForceCloseStopLoss, mtp, repayAmount, "") - } else { - return errors.Wrap(err, "error executing force close") - } - - return nil -} - -func (k Keeper) CheckAndCloseAtTakeProfit(ctx sdk.Context, mtp *types.MTP, pool types.Pool, baseCurrency string) error { - defer func() { - if r := recover(); r != nil { - if msg, ok := r.(string); ok { - ctx.Logger().Error(msg) - } - } - }() - tradingAssetPrice, err := k.GetAssetPrice(ctx, mtp.TradingAsset) if err != nil { return err } - if mtp.Position == types.Position_LONG { - if !tradingAssetPrice.GTE(mtp.TakeProfitPrice) { - return fmt.Errorf("mtp take profit price is not >= token price") - } - } else { - if !tradingAssetPrice.LTE(mtp.TakeProfitPrice) { - return fmt.Errorf("mtp take profit price is not <= token price") - } + if forceClosed { + k.EmitForceClose(ctx, "unhealthy", *mtp, repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, closer, allInterestsPaid, tradingAssetPrice) + return nil } - var repayAmount math.Int - switch mtp.Position { - case types.Position_LONG: - repayAmount, err = k.ForceCloseLong(ctx, mtp, &pool, true, baseCurrency) - case types.Position_SHORT: - repayAmount, err = k.ForceCloseShort(ctx, mtp, &pool, true, baseCurrency) - default: - return errors.Wrap(types.ErrInvalidPosition, fmt.Sprintf("invalid position type: %s", mtp.Position)) + if mtp.CheckForStopLoss(tradingAssetPrice) { + repayAmt, returnAmt, err = k.ForceClose(ctx, mtp, &pool, ammPool) + if err != nil { + return sdkerrors.Wrap(err, "error executing force close") + } + k.EmitForceClose(ctx, "stop_loss", *mtp, repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, closer, allInterestsPaid, tradingAssetPrice) + return nil } - if err == nil { - // Emit event if position was closed - k.EmitForceClose(ctx, types.EventForceCloseTakeprofit, mtp, repayAmount, "") - } else { - return errors.Wrap(err, "error executing force close") + if mtp.CheckForTakeProfit(tradingAssetPrice) { + repayAmt, returnAmt, err = k.ForceClose(ctx, mtp, &pool, ammPool) + if err != nil { + return sdkerrors.Wrap(err, "error executing force close") + } + k.EmitForceClose(ctx, "take_profit", *mtp, repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, closer, allInterestsPaid, tradingAssetPrice) + return nil } - return nil + return errors.New("position cannot be liquidated") } diff --git a/x/perpetual/keeper/process_mtp_test.go b/x/perpetual/keeper/process_mtp_test.go index ef2e4e927..90d20b354 100644 --- a/x/perpetual/keeper/process_mtp_test.go +++ b/x/perpetual/keeper/process_mtp_test.go @@ -146,13 +146,13 @@ func (suite *PerpetualKeeperTestSuite) TestCheckAndLiquidateUnhealthyPosition() params = mk.GetParams(ctx) params.BorrowInterestPaymentFundAddress = addr[2].String() params.BorrowInterestPaymentFundPercentage = sdkmath.LegacyMustNewDecFromStr("0.5") - mk.SetParams(ctx, ¶ms) - + err = mk.SetParams(ctx, ¶ms) + suite.Require().NoError(err) mtp := mtps[0] perpPool, _ := mk.GetPool(ctx, pool.PoolId) - err = mk.CheckAndLiquidateUnhealthyPosition(ctx, &mtp, perpPool, pool, ptypes.BaseCurrency) + err = mk.CheckAndLiquidatePosition(ctx, &mtp, perpPool, &pool, ptypes.BaseCurrency) suite.Require().NoError(err) // Set borrow interest rate to 100% to test liquidation @@ -187,14 +187,14 @@ func (suite *PerpetualKeeperTestSuite) TestCheckAndLiquidateUnhealthyPosition() StopLossPrice: sdkmath.LegacyZeroDec(), }, mtp) - err = mk.CheckAndLiquidateUnhealthyPosition(ctx, &mtp, perpPool, pool, ptypes.BaseCurrency) + err = mk.CheckAndLiquidatePosition(ctx, &mtp, perpPool, &pool, "") suite.Require().NoError(err) mtps = mk.GetAllMTPs(ctx) suite.Require().Equal(len(mtps), 0) } -func TestCheckAndCloseAtTakeProfit(t *testing.T) { +func TestCheckAndLiquidatePosition(t *testing.T) { app := simapp.InitElysTestApp(true, t) ctx := app.BaseApp.NewContext(true) simapp.SetStakingParam(app, ctx) @@ -328,7 +328,7 @@ func TestCheckAndCloseAtTakeProfit(t *testing.T) { perpPool, _ := mk.GetPool(ctx, pool.PoolId) - err = mk.CheckAndCloseAtTakeProfit(ctx, &mtp, perpPool, ptypes.BaseCurrency) + err = mk.CheckAndLiquidatePosition(ctx, &mtp, perpPool, &pool, "") require.Error(t, err) // Set price above target price @@ -341,7 +341,7 @@ func TestCheckAndCloseAtTakeProfit(t *testing.T) { Timestamp: uint64(ctx.BlockTime().Unix()), }) - err = mk.CheckAndCloseAtTakeProfit(ctx, &mtp, perpPool, ptypes.BaseCurrency) + err = mk.CheckAndLiquidatePosition(ctx, &mtp, perpPool, &pool, "") require.NoError(t, err) mtps = mk.GetAllMTPs(ctx) @@ -501,7 +501,7 @@ func (suite *PerpetualKeeperTestSuite) TestCheckAndLiquidateStopLossPosition() { perpPool, _ := mk.GetPool(ctx, ammPool.PoolId) - err = mk.CheckAndCloseAtStopLoss(ctx, &mtp, perpPool, ptypes.BaseCurrency) + err = mk.CheckAndLiquidatePosition(ctx, &mtp, perpPool, &ammPool, "") suite.Require().NoError(err) mtps = mk.GetAllMTPs(ctx) diff --git a/x/perpetual/keeper/query_close_estimation.go b/x/perpetual/keeper/query_close_estimation.go index 31cd8f185..d2e115382 100644 --- a/x/perpetual/keeper/query_close_estimation.go +++ b/x/perpetual/keeper/query_close_estimation.go @@ -46,7 +46,7 @@ func (k Keeper) HandleCloseEstimation(ctx sdk.Context, req *types.QueryCloseEsti } k.UpdateMTPBorrowInterestUnpaidLiability(ctx, &mtp) - err = k.UpdateFundingFee(ctx, &mtp, &pool, ammPool) + _, _, _, err = k.UpdateFundingFee(ctx, &mtp, &pool) if err != nil { return nil, err } diff --git a/x/perpetual/keeper/settle_funding_fee.go b/x/perpetual/keeper/settle_funding_fee.go index 5fa87299a..5a4b08b3a 100644 --- a/x/perpetual/keeper/settle_funding_fee.go +++ b/x/perpetual/keeper/settle_funding_fee.go @@ -1,45 +1,45 @@ package keeper import ( + "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" - ammtypes "github.com/elys-network/elys/x/amm/types" "github.com/elys-network/elys/x/perpetual/types" ) -func (k Keeper) UpdateFundingFee(ctx sdk.Context, mtp *types.MTP, pool *types.Pool, ammPool ammtypes.Pool) error { +func (k Keeper) UpdateFundingFee(ctx sdk.Context, mtp *types.MTP, pool *types.Pool) (bool, math.Int, math.Int, error) { - err := k.FundingFeeCollection(ctx, mtp, pool, ammPool) + fullFundingFeePayment, fundingFeeAmt, err := k.FundingFeeCollection(ctx, mtp, pool) if err != nil { - return err + return fullFundingFeePayment, fundingFeeAmt, math.ZeroInt(), err } - err = k.FundingFeeDistribution(ctx, mtp, pool, ammPool) + amountDistributed, err := k.FundingFeeDistribution(ctx, mtp, pool) if err != nil { - return err + return fullFundingFeePayment, fundingFeeAmt, amountDistributed, err } mtp.LastFundingCalcBlock = uint64(ctx.BlockHeight()) mtp.LastFundingCalcTime = uint64(ctx.BlockTime().Unix()) - return nil + return fullFundingFeePayment, fundingFeeAmt, amountDistributed, nil } // SettleFunding handles funding fee collection and distribution -func (k Keeper) SettleFunding(ctx sdk.Context, mtp *types.MTP, pool *types.Pool, ammPool ammtypes.Pool) error { +func (k Keeper) SettleFunding(ctx sdk.Context, mtp *types.MTP, pool *types.Pool) (bool, math.Int, math.Int, error) { - err := k.UpdateFundingFee(ctx, mtp, pool, ammPool) + fundingFeeFullyPaid, fundingFeeAmt, amountDistributed, err := k.UpdateFundingFee(ctx, mtp, pool) if err != nil { - return err + return fundingFeeFullyPaid, fundingFeeAmt, amountDistributed, err } // apply changes to mtp object err = k.SetMTP(ctx, mtp) if err != nil { - return err + return fundingFeeFullyPaid, fundingFeeAmt, amountDistributed, err } // apply changes to pool object k.SetPool(ctx, *pool) - return nil + return fundingFeeFullyPaid, fundingFeeAmt, amountDistributed, nil } diff --git a/x/perpetual/keeper/settle_funding_fee_collection.go b/x/perpetual/keeper/settle_funding_fee_collection.go index 206f0c656..58f41c6bb 100644 --- a/x/perpetual/keeper/settle_funding_fee_collection.go +++ b/x/perpetual/keeper/settle_funding_fee_collection.go @@ -1,25 +1,33 @@ package keeper import ( + "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" - ammtypes "github.com/elys-network/elys/x/amm/types" "github.com/elys-network/elys/x/perpetual/types" ) -func (k Keeper) FundingFeeCollection(ctx sdk.Context, mtp *types.MTP, pool *types.Pool, ammPool ammtypes.Pool) error { +func (k Keeper) FundingFeeCollection(ctx sdk.Context, mtp *types.MTP, pool *types.Pool) (bool, math.Int, error) { + + fullFundingFeePayment := true + takeAmountCustodyAmount := math.ZeroInt() // get funding rate longRate, shortRate := k.GetFundingRate(ctx, mtp.LastFundingCalcBlock, mtp.LastFundingCalcTime, mtp.AmmPoolId) if mtp.Position == types.Position_LONG { - takeAmountCustodyAmount := types.CalcTakeAmount(mtp.Custody, longRate) + takeAmountCustodyAmount = types.CalcTakeAmount(mtp.Custody, longRate) if !takeAmountCustodyAmount.IsPositive() { - return nil + return true, math.ZeroInt(), nil + } + + if takeAmountCustodyAmount.GT(mtp.Custody) { + fullFundingFeePayment = false + takeAmountCustodyAmount = mtp.Custody } // increase fees collected err := pool.UpdateFeesCollected(mtp.CustodyAsset, takeAmountCustodyAmount, true) if err != nil { - return err + return fullFundingFeePayment, math.ZeroInt(), err } // update mtp custody @@ -31,43 +39,48 @@ func (k Keeper) FundingFeeCollection(ctx sdk.Context, mtp *types.MTP, pool *type // update pool custody balance err = pool.UpdateCustody(mtp.CustodyAsset, takeAmountCustodyAmount, false, mtp.Position) if err != nil { - return err + return fullFundingFeePayment, math.ZeroInt(), err } } else { takeAmountLiabilityAmount := types.CalcTakeAmount(mtp.Liabilities, shortRate) if !takeAmountLiabilityAmount.IsPositive() { - return nil + return true, math.ZeroInt(), nil } // increase fees collected // Note: fees is collected in liabilities asset err := pool.UpdateFeesCollected(mtp.LiabilitiesAsset, takeAmountLiabilityAmount, true) if err != nil { - return err + return fullFundingFeePayment, math.ZeroInt(), err } tradingAssetPrice, err := k.GetAssetPrice(ctx, mtp.TradingAsset) if err != nil { - return err + return fullFundingFeePayment, math.ZeroInt(), err } // should be done in custody // short -> usdc // long -> custody // For short, takeAmountLiabilityAmount is in trading asset, need to convert to custody asset which is in usdc - custodyAmt := takeAmountLiabilityAmount.ToLegacyDec().Mul(tradingAssetPrice).TruncateInt() + takeAmountCustodyAmount = takeAmountLiabilityAmount.ToLegacyDec().Mul(tradingAssetPrice).TruncateInt() + + if takeAmountCustodyAmount.GT(mtp.Custody) { + fullFundingFeePayment = false + takeAmountCustodyAmount = mtp.Custody + } // update mtp custody - mtp.Custody = mtp.Custody.Sub(custodyAmt) + mtp.Custody = mtp.Custody.Sub(takeAmountCustodyAmount) // add payment to total funding fee paid in custody asset - mtp.FundingFeePaidCustody = mtp.FundingFeePaidCustody.Add(custodyAmt) + mtp.FundingFeePaidCustody = mtp.FundingFeePaidCustody.Add(takeAmountCustodyAmount) // update pool custody balance - err = pool.UpdateCustody(mtp.CustodyAsset, custodyAmt, false, mtp.Position) + err = pool.UpdateCustody(mtp.CustodyAsset, takeAmountCustodyAmount, false, mtp.Position) if err != nil { - return err + return fullFundingFeePayment, math.ZeroInt(), err } } - return nil + return fullFundingFeePayment, takeAmountCustodyAmount, nil } diff --git a/x/perpetual/keeper/settle_funding_fee_distribution.go b/x/perpetual/keeper/settle_funding_fee_distribution.go index 59a2a6184..36ff4e0a4 100644 --- a/x/perpetual/keeper/settle_funding_fee_distribution.go +++ b/x/perpetual/keeper/settle_funding_fee_distribution.go @@ -2,51 +2,52 @@ package keeper import ( sdkmath "cosmossdk.io/math" - "fmt" + "errors" sdk "github.com/cosmos/cosmos-sdk/types" - ammtypes "github.com/elys-network/elys/x/amm/types" "github.com/elys-network/elys/x/perpetual/types" ) -func (k Keeper) FundingFeeDistribution(ctx sdk.Context, mtp *types.MTP, pool *types.Pool, ammPool ammtypes.Pool) error { +func (k Keeper) FundingFeeDistribution(ctx sdk.Context, mtp *types.MTP, pool *types.Pool) (sdkmath.Int, error) { totalLongOpenInterest := pool.GetTotalLongOpenInterest() totalShortOpenInterest := pool.GetTotalShortOpenInterest() // Total fund collected should be - long, short := k.GetFundingDistributionValue(ctx, uint64(ctx.BlockHeight()), pool.AmmPoolId) + long, short := k.GetFundingDistributionValue(ctx, mtp.LastFundingCalcBlock, pool.AmmPoolId) var totalFund sdkmath.LegacyDec // calc funding fee share var fundingFeeShare sdkmath.LegacyDec + amountDistributed := sdkmath.ZeroInt() if mtp.Position == types.Position_LONG { // Ensure liabilitiesLong is not zero to avoid division by zero if totalLongOpenInterest.IsZero() { - return fmt.Errorf("totalCustodyLong in FundingFeeDistribution cannot be zero") + return amountDistributed, errors.New("totalLongOpenInterest in FundingFeeDistribution cannot be zero") } fundingFeeShare = mtp.Custody.ToLegacyDec().Quo(totalLongOpenInterest.ToLegacyDec()) totalFund = short // if funding fee share is zero, skip mtp if fundingFeeShare.IsZero() || totalFund.IsZero() { - return nil + return amountDistributed, nil } // calculate funding fee amount fundingFeeAmount := totalFund.Mul(fundingFeeShare) + amountDistributed = fundingFeeAmount.TruncateInt() // update mtp custody mtp.Custody = mtp.Custody.Add(fundingFeeAmount.TruncateInt()) // decrease fees collected err := pool.UpdateFeesCollected(mtp.CustodyAsset, fundingFeeAmount.TruncateInt(), false) if err != nil { - return err + return sdkmath.ZeroInt(), err } // update pool custody balance err = pool.UpdateCustody(mtp.CustodyAsset, fundingFeeAmount.TruncateInt(), true, mtp.Position) if err != nil { - return err + return sdkmath.ZeroInt(), err } // add payment to total funding fee paid in custody asset @@ -54,14 +55,14 @@ func (k Keeper) FundingFeeDistribution(ctx sdk.Context, mtp *types.MTP, pool *ty } else { // Ensure liabilitiesShort is not zero to avoid division by zero if totalShortOpenInterest.IsZero() { - return types.ErrAmountTooLow + return amountDistributed, errors.New("totalShortOpenInterest in FundingFeeDistribution cannot be zero") } fundingFeeShare = mtp.Liabilities.ToLegacyDec().Quo(totalShortOpenInterest.ToLegacyDec()) totalFund = long // if funding fee share is zero, skip mtp if fundingFeeShare.IsZero() || totalFund.IsZero() { - return nil + return amountDistributed, nil } // calculate funding fee amount @@ -69,34 +70,35 @@ func (k Keeper) FundingFeeDistribution(ctx sdk.Context, mtp *types.MTP, pool *ty // adding case for fundingFeeAmount being smaller tha 10^-18 if fundingFeeAmount.IsZero() { - return nil + return amountDistributed, nil } // decrease fees collected err := pool.UpdateFeesCollected(mtp.LiabilitiesAsset, fundingFeeAmount, false) if err != nil { - return err + return amountDistributed, err } tradingAssetPrice, err := k.GetAssetPrice(ctx, mtp.TradingAsset) if err != nil { - return err + return amountDistributed, err } // For short, fundingFeeAmount is in trading asset, need to convert to custody asset which is in usdc custodyAmt := fundingFeeAmount.ToLegacyDec().Mul(tradingAssetPrice).TruncateInt() + amountDistributed = custodyAmt // update mtp Custody mtp.Custody = mtp.Custody.Add(custodyAmt) // update pool liability balance err = pool.UpdateCustody(mtp.CustodyAsset, custodyAmt, true, mtp.Position) if err != nil { - return err + return sdkmath.ZeroInt(), err } // add payment to total funding fee paid in custody asset mtp.FundingFeeReceivedCustody = mtp.FundingFeeReceivedCustody.Add(custodyAmt) } - return nil + return amountDistributed, nil } diff --git a/x/perpetual/types/events.go b/x/perpetual/types/events.go index a2e4b9dcc..344be3aba 100644 --- a/x/perpetual/types/events.go +++ b/x/perpetual/types/events.go @@ -1,12 +1,10 @@ package types const ( - EventOpen = "perpetual/mtp_open" - EventClose = "perpetual/mtp_close" - EventForceCloseUnhealthy = "perpetual/mtp_force_close_unhealthy" - EventForceCloseStopLoss = "perpetual/mtp_force_close_stopLoss" - EventForceCloseTakeprofit = "perpetual/mtp_force_close_takeprofit" - EventIncrementalPayFund = "perpetual/incremental_pay_fund" - EventRepayFund = "perpetual/repay_fund" - EventClosePositions = "perpetual/close_positions" + EventOpen = "perpetual/mtp_open" + EventUpdateStopLoss = "perpetual/update_stop_loss" + EventUpdateTakeProfitPrice = "perpetual/update_take_profit_price" + EventOpenConsolidate = "perpetual/mtp_open_consolidate" + EventClose = "perpetual/mtp_close" + EventForceClosed = "perpetual/mtp_force_closed" ) diff --git a/x/perpetual/types/generate_close_event.go b/x/perpetual/types/generate_close_event.go deleted file mode 100644 index 18f549094..000000000 --- a/x/perpetual/types/generate_close_event.go +++ /dev/null @@ -1,36 +0,0 @@ -package types - -import ( - "strconv" - - "cosmossdk.io/math" - sdk "github.com/cosmos/cosmos-sdk/types" -) - -func GenerateCloseEvent(mtp *MTP, repayAmount math.Int, closingRatio math.LegacyDec) sdk.Event { - return sdk.NewEvent(EventClose, - sdk.NewAttribute("address", mtp.Address), - sdk.NewAttribute("collateral_asset", mtp.CollateralAsset), - sdk.NewAttribute("trading_asset", mtp.TradingAsset), - sdk.NewAttribute("liabilities_asset", mtp.LiabilitiesAsset), - sdk.NewAttribute("custody_asset", mtp.CustodyAsset), - sdk.NewAttribute("collateral", mtp.Collateral.String()), - sdk.NewAttribute("liabilities", mtp.Liabilities.String()), - sdk.NewAttribute("borrow_interest_unpaid_liability", mtp.BorrowInterestUnpaidLiability.String()), - sdk.NewAttribute("borrow_interest_paid_custody", mtp.BorrowInterestPaidCustody.String()), - sdk.NewAttribute("custody", mtp.Custody.String()), - sdk.NewAttribute("take_profit_liabilities", mtp.TakeProfitLiabilities.String()), - sdk.NewAttribute("take_profit_custody", mtp.TakeProfitCustody.String()), - sdk.NewAttribute("mtp_health", mtp.MtpHealth.String()), - sdk.NewAttribute("position", mtp.Position.String()), - sdk.NewAttribute("id", strconv.FormatInt(int64(mtp.Id), 10)), - sdk.NewAttribute("amm_pool_id", strconv.FormatInt(int64(mtp.AmmPoolId), 10)), - sdk.NewAttribute("take_profit_price", mtp.TakeProfitPrice.String()), - sdk.NewAttribute("take_profit_borrow_factor", mtp.TakeProfitBorrowFactor.String()), - sdk.NewAttribute("funding_fee_paid_custody", mtp.FundingFeePaidCustody.String()), - sdk.NewAttribute("funding_fee_received_custody", mtp.FundingFeeReceivedCustody.String()), - sdk.NewAttribute("open_price", mtp.OpenPrice.String()), - sdk.NewAttribute("repay_amount", repayAmount.String()), - sdk.NewAttribute("closing_ratio", closingRatio.String()), - ) -} diff --git a/x/perpetual/types/generate_open_event.go b/x/perpetual/types/generate_open_event.go deleted file mode 100644 index 50210a6bc..000000000 --- a/x/perpetual/types/generate_open_event.go +++ /dev/null @@ -1,33 +0,0 @@ -package types - -import ( - "strconv" - - sdk "github.com/cosmos/cosmos-sdk/types" -) - -func GenerateOpenEvent(mtp *MTP) sdk.Event { - return sdk.NewEvent(EventOpen, - sdk.NewAttribute("address", mtp.Address), - sdk.NewAttribute("collateral_asset", mtp.CollateralAsset), - sdk.NewAttribute("trading_asset", mtp.TradingAsset), - sdk.NewAttribute("liabilities_asset", mtp.LiabilitiesAsset), - sdk.NewAttribute("custody_asset", mtp.CustodyAsset), - sdk.NewAttribute("collateral", mtp.Collateral.String()), - sdk.NewAttribute("liabilities", mtp.Liabilities.String()), - sdk.NewAttribute("borrow_interest_paid_custody", mtp.BorrowInterestPaidCustody.String()), - sdk.NewAttribute("borrow_interest_unpaid_liability", mtp.BorrowInterestUnpaidLiability.String()), - sdk.NewAttribute("custody", mtp.Custody.String()), - sdk.NewAttribute("take_profit_liabilities", mtp.TakeProfitLiabilities.String()), - sdk.NewAttribute("take_profit_custody", mtp.TakeProfitCustody.String()), - sdk.NewAttribute("mtp_health", mtp.MtpHealth.String()), - sdk.NewAttribute("position", mtp.Position.String()), - sdk.NewAttribute("id", strconv.FormatInt(int64(mtp.Id), 10)), - sdk.NewAttribute("amm_pool_id", strconv.FormatInt(int64(mtp.AmmPoolId), 10)), - sdk.NewAttribute("take_profit_price", mtp.TakeProfitPrice.String()), - sdk.NewAttribute("take_profit_borrow_factor", mtp.TakeProfitBorrowFactor.String()), - sdk.NewAttribute("funding_fee_paid_custody", mtp.FundingFeePaidCustody.String()), - sdk.NewAttribute("funding_fee_received_custody", mtp.FundingFeeReceivedCustody.String()), - sdk.NewAttribute("open_price", mtp.OpenPrice.String()), - ) -} diff --git a/x/perpetual/types/generate_open_event_test.go b/x/perpetual/types/generate_open_event_test.go deleted file mode 100644 index 1cc711981..000000000 --- a/x/perpetual/types/generate_open_event_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package types_test - -import ( - sdkmath "cosmossdk.io/math" - "strconv" - "testing" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/elys-network/elys/x/perpetual/types" - "github.com/stretchr/testify/assert" -) - -func TestGenerateOpenEvent(t *testing.T) { - // Mock data for testing - testMTP := types.MTP{ - Address: "elys1x0jyazg9qzys8x9m2x8q3q3x0jyazg9qzys8x9", - CollateralAsset: "uusdc", - TradingAsset: "uatom", - LiabilitiesAsset: "uusdc", - CustodyAsset: "uatom", - Collateral: sdkmath.OneInt(), - Liabilities: sdkmath.OneInt(), - BorrowInterestPaidCustody: sdkmath.OneInt(), - BorrowInterestUnpaidLiability: sdkmath.OneInt(), - Custody: sdkmath.OneInt(), - TakeProfitLiabilities: sdkmath.OneInt(), - TakeProfitCustody: sdkmath.OneInt(), - MtpHealth: sdkmath.LegacyZeroDec(), - Position: types.Position_LONG, - Id: 1, - AmmPoolId: 1, - TakeProfitPrice: sdkmath.LegacyNewDec(10), - TakeProfitBorrowFactor: sdkmath.LegacyOneDec(), - FundingFeePaidCustody: sdkmath.OneInt(), - FundingFeeReceivedCustody: sdkmath.OneInt(), - OpenPrice: sdkmath.LegacyNewDec(10), - StopLossPrice: sdkmath.LegacyNewDec(0), - } - - event := types.GenerateOpenEvent(&testMTP) - - // Assert that the event type is correct - assert.Equal(t, types.EventOpen, event.Type) - - // Assert that all the attributes are correctly set - assert.Equal(t, testMTP.Address, getAttributeValue(event, "address")) - assert.Equal(t, testMTP.CollateralAsset, getAttributeValue(event, "collateral_asset")) - assert.Equal(t, testMTP.TradingAsset, getAttributeValue(event, "trading_asset")) - assert.Equal(t, testMTP.LiabilitiesAsset, getAttributeValue(event, "liabilities_asset")) - assert.Equal(t, testMTP.CustodyAsset, getAttributeValue(event, "custody_asset")) - assert.Equal(t, testMTP.Collateral.String(), getAttributeValue(event, "collateral")) - assert.Equal(t, testMTP.Liabilities.String(), getAttributeValue(event, "liabilities")) - assert.Equal(t, testMTP.BorrowInterestPaidCustody.String(), getAttributeValue(event, "borrow_interest_paid_custody")) - assert.Equal(t, testMTP.BorrowInterestUnpaidLiability.String(), getAttributeValue(event, "borrow_interest_unpaid_liability")) - assert.Equal(t, testMTP.Custody.String(), getAttributeValue(event, "custody")) - assert.Equal(t, testMTP.TakeProfitLiabilities.String(), getAttributeValue(event, "take_profit_liabilities")) - assert.Equal(t, testMTP.TakeProfitCustody.String(), getAttributeValue(event, "take_profit_custody")) - assert.Equal(t, testMTP.MtpHealth.String(), getAttributeValue(event, "mtp_health")) - assert.Equal(t, testMTP.Position.String(), getAttributeValue(event, "position")) - assert.Equal(t, strconv.FormatInt(int64(testMTP.Id), 10), getAttributeValue(event, "id")) - assert.Equal(t, strconv.FormatInt(int64(testMTP.AmmPoolId), 10), getAttributeValue(event, "amm_pool_id")) - assert.Equal(t, testMTP.TakeProfitPrice.String(), getAttributeValue(event, "take_profit_price")) - assert.Equal(t, testMTP.TakeProfitBorrowFactor.String(), getAttributeValue(event, "take_profit_borrow_factor")) - assert.Equal(t, testMTP.FundingFeePaidCustody.String(), getAttributeValue(event, "funding_fee_paid_custody")) - assert.Equal(t, testMTP.FundingFeeReceivedCustody.String(), getAttributeValue(event, "funding_fee_received_custody")) - assert.Equal(t, testMTP.OpenPrice.String(), getAttributeValue(event, "open_price")) -} - -// Helper function to get attribute value from an event -func getAttributeValue(event sdk.Event, key string) string { - for _, attr := range event.Attributes { - if attr.Key == key { - return attr.Value - } - } - return "" -} diff --git a/x/perpetual/types/genesis.go b/x/perpetual/types/genesis.go index 8ac61202b..fd91db02d 100644 --- a/x/perpetual/types/genesis.go +++ b/x/perpetual/types/genesis.go @@ -1,7 +1,7 @@ package types import ( - "fmt" + "errors" ) // DefaultIndex is the default global index @@ -27,7 +27,7 @@ func (gs GenesisState) Validate() error { for _, elem := range gs.PoolList { index := elem.AmmPoolId if found := poolIndexMap[elem.AmmPoolId]; found { - return fmt.Errorf("duplicated index for pool") + return errors.New("duplicated index for pool") } poolIndexMap[index] = true } @@ -38,7 +38,7 @@ func (gs GenesisState) Validate() error { key := GetMTPKey(elem.GetAccountAddress(), elem.Id) index := string(key) if _, ok := mtpIndexMap[index]; ok { - return fmt.Errorf("duplicated index for pool") + return errors.New("duplicated index for pool") } mtpIndexMap[index] = struct{}{} } @@ -48,7 +48,7 @@ func (gs GenesisState) Validate() error { for _, elem := range gs.AddressWhitelist { index := elem if _, ok := mtpIndexMap[index]; ok { - return fmt.Errorf("duplicated index for pool") + return errors.New("duplicated index for pool") } whitelistMap[index] = struct{}{} } diff --git a/x/perpetual/types/message_close_positions.go b/x/perpetual/types/message_close_positions.go index b4e434cbe..2ff664418 100644 --- a/x/perpetual/types/message_close_positions.go +++ b/x/perpetual/types/message_close_positions.go @@ -1,8 +1,9 @@ package types import ( + "errors" + errorsmod "cosmossdk.io/errors" - "fmt" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) @@ -24,7 +25,7 @@ func (msg *MsgClosePositions) ValidateBasic() error { } if len(msg.Liquidate) == 0 && len(msg.StopLoss) == 0 && len(msg.TakeProfit) == 0 { - return fmt.Errorf("liquidate, stop loss, take profit all are empty") + return errors.New("liquidate, stop loss, take profit all are empty") } for _, position := range msg.Liquidate { diff --git a/x/perpetual/types/message_open.go b/x/perpetual/types/message_open.go index e8a2e0370..6a4e5275d 100644 --- a/x/perpetual/types/message_open.go +++ b/x/perpetual/types/message_open.go @@ -1,9 +1,10 @@ package types import ( + "errors" + errorsmod "cosmossdk.io/errors" sdkmath "cosmossdk.io/math" - "fmt" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) @@ -38,7 +39,7 @@ func (msg *MsgOpen) ValidateBasic() error { } if msg.PoolId == 0 { - return fmt.Errorf("pool id cannot be 0") + return errors.New("pool id cannot be 0") } if !(msg.Leverage.GT(sdkmath.LegacyOneDec()) || msg.Leverage.IsZero()) { @@ -59,10 +60,10 @@ func (msg *MsgOpen) ValidateBasic() error { return err } if msg.Position == Position_LONG && !msg.StopLossPrice.IsZero() && msg.TakeProfitPrice.LTE(msg.StopLossPrice) { - return fmt.Errorf("TakeProfitPrice cannot be <= StopLossPrice for LONG") + return errors.New("TakeProfitPrice cannot be <= StopLossPrice for LONG") } if msg.Position == Position_SHORT && !msg.StopLossPrice.IsZero() && msg.TakeProfitPrice.GTE(msg.StopLossPrice) { - return fmt.Errorf("TakeProfitPrice cannot be >= StopLossPrice for SHORT") + return errors.New("TakeProfitPrice cannot be >= StopLossPrice for SHORT") } return nil } diff --git a/x/perpetual/types/params.go b/x/perpetual/types/params.go index f599359e8..5705be6e3 100644 --- a/x/perpetual/types/params.go +++ b/x/perpetual/types/params.go @@ -1,6 +1,7 @@ package types import ( + "errors" "fmt" "cosmossdk.io/math" @@ -60,7 +61,7 @@ func (p Params) Validate() error { return err } if p.BorrowInterestRateMin.GT(p.BorrowInterestRateMax) { - return fmt.Errorf("BorrowInterestRateMin must be less than BorrowInterestRateMax") + return errors.New("BorrowInterestRateMin must be less than BorrowInterestRateMax") } if err := CheckLegacyDecNilAndNegative(p.BorrowInterestRateIncrease, "BorrowInterestRateIncrease"); err != nil { return err @@ -87,22 +88,22 @@ func (p Params) Validate() error { return err } if p.MaximumLongTakeProfitPriceRatio.LTE(math.LegacyOneDec()) { - return fmt.Errorf("MaximumLongTakeProfitPriceRatio must be greater than 1") + return errors.New("MaximumLongTakeProfitPriceRatio must be greater than 1") } if err := CheckLegacyDecNilAndNegative(p.MaximumShortTakeProfitPriceRatio, "MaximumShortTakeProfitPriceRatio"); err != nil { return err } if p.MaximumShortTakeProfitPriceRatio.GTE(math.LegacyOneDec()) { - return fmt.Errorf("MaximumShortTakeProfitPriceRatio must be less than 1") + return errors.New("MaximumShortTakeProfitPriceRatio must be less than 1") } if err := CheckLegacyDecNilAndNegative(p.MinimumLongTakeProfitPriceRatio, "MinimumLongTakeProfitPriceRatio"); err != nil { return err } if p.MinimumLongTakeProfitPriceRatio.LTE(math.LegacyOneDec()) { - return fmt.Errorf("MinimumLongTakeProfitPriceRatio must be greater than 1") + return errors.New("MinimumLongTakeProfitPriceRatio must be greater than 1") } if p.MaximumLongTakeProfitPriceRatio.LTE(p.MinimumLongTakeProfitPriceRatio) { - return fmt.Errorf("MaximumLongTakeProfitPriceRatio must be greater than MinimumLongTakeProfitPriceRatio") + return errors.New("MaximumLongTakeProfitPriceRatio must be greater than MinimumLongTakeProfitPriceRatio") } if err := CheckLegacyDecNilAndNegative(p.PerpetualSwapFee, "PerpetualSwapFee"); err != nil { return err @@ -118,7 +119,7 @@ func (p Params) Validate() error { } if containsDuplicates(p.EnabledPools) { - return fmt.Errorf("array must not contain duplicate values") + return errors.New("array must not contain duplicate values") } return nil } diff --git a/x/perpetual/types/types.go b/x/perpetual/types/types.go index c7b72abf6..dc4defe51 100644 --- a/x/perpetual/types/types.go +++ b/x/perpetual/types/types.go @@ -1,7 +1,7 @@ package types import ( - "fmt" + "errors" errorsmod "cosmossdk.io/errors" sdkmath "cosmossdk.io/math" @@ -103,7 +103,7 @@ func (mtp MTP) GetBorrowInterestAmountAsCustodyAsset(tradingAssetPrice sdkmath.L borrowInterestPaymentInCustody := sdkmath.ZeroInt() if mtp.Position == Position_LONG { if tradingAssetPrice.IsZero() { - return sdkmath.ZeroInt(), fmt.Errorf("trading asset price is zero") + return sdkmath.ZeroInt(), errors.New("trading asset price is zero") } // liabilities are in usdc, custody is in trading asset borrowInterestPaymentInCustody = mtp.BorrowInterestUnpaidLiability.ToLegacyDec().Quo(tradingAssetPrice).TruncateInt() @@ -113,3 +113,25 @@ func (mtp MTP) GetBorrowInterestAmountAsCustodyAsset(tradingAssetPrice sdkmath.L } return borrowInterestPaymentInCustody, nil } + +func (mtp MTP) CheckForStopLoss(tradingAssetPrice sdkmath.LegacyDec) bool { + stopLossReached := false + if mtp.Position == Position_LONG { + stopLossReached = !mtp.StopLossPrice.IsNil() && tradingAssetPrice.LTE(mtp.StopLossPrice) + } + if mtp.Position == Position_SHORT { + stopLossReached = !mtp.StopLossPrice.IsNil() && tradingAssetPrice.GTE(mtp.StopLossPrice) + } + return stopLossReached +} + +func (mtp MTP) CheckForTakeProfit(tradingAssetPrice sdkmath.LegacyDec) bool { + takeProfitReached := false + if mtp.Position == Position_LONG { + takeProfitReached = !mtp.TakeProfitPrice.IsNil() && tradingAssetPrice.GTE(mtp.TakeProfitPrice) + } + if mtp.Position == Position_SHORT { + takeProfitReached = !mtp.TakeProfitPrice.IsNil() && tradingAssetPrice.LTE(mtp.TakeProfitPrice) + } + return takeProfitReached +} diff --git a/x/perpetual/types/types_test.go b/x/perpetual/types/types_test.go new file mode 100644 index 000000000..57dd4cf7d --- /dev/null +++ b/x/perpetual/types/types_test.go @@ -0,0 +1,71 @@ +package types_test + +import ( + "cosmossdk.io/math" + "github.com/elys-network/elys/x/perpetual/types" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCheckForStopLoss(t *testing.T) { + mtp := types.MTP{} + + mtp.Position = types.Position_LONG + mtp.StopLossPrice = math.LegacyNewDec(10) + assert.False(t, mtp.CheckForStopLoss(math.LegacyMustNewDecFromStr("10.1"))) // Above StopLossPrice + assert.True(t, mtp.CheckForStopLoss(math.LegacyNewDec(10))) // Equal to StopLossPrice + assert.True(t, mtp.CheckForStopLoss(math.LegacyMustNewDecFromStr("9.9"))) // Below StopLossPrice + + mtp.Position = types.Position_SHORT + mtp.StopLossPrice = math.LegacyNewDec(10) + assert.False(t, mtp.CheckForStopLoss(math.LegacyMustNewDecFromStr("9.9"))) // Below StopLossPrice + assert.True(t, mtp.CheckForStopLoss(math.LegacyNewDec(10))) // Equal to StopLossPrice + assert.True(t, mtp.CheckForStopLoss(math.LegacyMustNewDecFromStr("10.1"))) // Above StopLossPrice + + assert.False(t, mtp.CheckForTakeProfit(math.LegacyNewDec(10))) // Should always return false + + // Edge case: Very high or low StopLossPrice + mtp.StopLossPrice = math.LegacyNewDec(1e6) // Unrealistically high stop loss + assert.False(t, mtp.CheckForStopLoss(math.LegacyNewDec(10))) + + mtp.StopLossPrice = math.LegacyNewDec(-1e6) // Unrealistically low stop loss + assert.True(t, mtp.CheckForStopLoss(math.LegacyNewDec(10))) + + // Test unknown position + mtp.Position = -1 // Invalid position + mtp.TakeProfitPrice = math.LegacyNewDec(10) +} + +func TestCheckForTakeProfit(t *testing.T) { + mtp := types.MTP{} + + // Test LONG position + mtp.Position = types.Position_LONG + mtp.TakeProfitPrice = math.LegacyNewDec(15) + + assert.False(t, mtp.CheckForTakeProfit(math.LegacyMustNewDecFromStr("14.9"))) // Below TakeProfitPrice + assert.True(t, mtp.CheckForTakeProfit(math.LegacyNewDec(15))) // Equal to TakeProfitPrice + assert.True(t, mtp.CheckForTakeProfit(math.LegacyMustNewDecFromStr("15.1"))) // Above TakeProfitPrice + + // Test SHORT position + mtp.Position = types.Position_SHORT + mtp.TakeProfitPrice = math.LegacyNewDec(10) + + assert.False(t, mtp.CheckForTakeProfit(math.LegacyMustNewDecFromStr("10.1"))) // Above TakeProfitPrice + assert.True(t, mtp.CheckForTakeProfit(math.LegacyNewDec(10))) // Equal to TakeProfitPrice + assert.True(t, mtp.CheckForTakeProfit(math.LegacyMustNewDecFromStr("9.9"))) // Below TakeProfitPrice + + // Test unknown position + mtp.Position = -1 // Invalid position + mtp.TakeProfitPrice = math.LegacyNewDec(10) + + assert.False(t, mtp.CheckForTakeProfit(math.LegacyNewDec(10))) // Should always return false + + // Edge case: Very high or low TakeProfitPrice + mtp.Position = types.Position_LONG + mtp.TakeProfitPrice = math.LegacyNewDec(1e6) // Unrealistically high take profit price + assert.False(t, mtp.CheckForTakeProfit(math.LegacyNewDec(10))) + + mtp.TakeProfitPrice = math.LegacyNewDec(-1e6) // Unrealistically low take profit price + assert.True(t, mtp.CheckForTakeProfit(math.LegacyNewDec(10))) +} diff --git a/x/tradeshield/types/events.go b/x/tradeshield/types/events.go index 2c4308e6d..1aace405a 100644 --- a/x/tradeshield/types/events.go +++ b/x/tradeshield/types/events.go @@ -139,7 +139,7 @@ func NewExecuteMarketBuySpotOrderEvt(order SpotOrder, res *ammtypes.MsgSwapByDen return sdk.NewEvent(TypeEvtExecuteMarketBuySpotOrder, sdk.NewAttribute("order_type", order.OrderType.String()), - sdk.NewAttribute("order_id", strconv.FormatInt(int64(order.OrderId), 10)), + sdk.NewAttribute("order_id", strconv.FormatUint(order.OrderId, 10)), sdk.NewAttribute("order_price", string(orderPrice)), sdk.NewAttribute("order_amount", order.OrderAmount.String()), sdk.NewAttribute("owner_address", order.OwnerAddress),