Skip to content

Commit

Permalink
Adding single perpetual trigger (#1105)
Browse files Browse the repository at this point in the history
* Improving MTP triggers for funding fee and interest payments

* fixing test cases

* improving test coverage

* Update x/perpetual/keeper/mtp_trigger_test.go

Co-authored-by: Amit Yadav <[email protected]>

* adding events for perpetual

---------

Co-authored-by: Amit Yadav <[email protected]>
  • Loading branch information
avkr003 and amityadav0 authored Jan 13, 2025
1 parent 9b5196f commit 19837d3
Show file tree
Hide file tree
Showing 42 changed files with 776 additions and 628 deletions.
2 changes: 2 additions & 0 deletions x/leveragelp/keeper/position_close.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions x/perpetual/client/cli/query_close_estimation.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package cli

import (
"cosmossdk.io/math"
"fmt"
"errors"
"strconv"

"github.com/cosmos/cosmos-sdk/client"
Expand All @@ -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)
Expand Down
41 changes: 26 additions & 15 deletions x/perpetual/keeper/close.go
Original file line number Diff line number Diff line change
@@ -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))
}
42 changes: 19 additions & 23 deletions x/perpetual/keeper/close_position.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -51,19 +47,19 @@ 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
if k.hooks != nil {
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
}
10 changes: 5 additions & 5 deletions x/perpetual/keeper/close_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
},
{
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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())
}
})
}
Expand Down
21 changes: 15 additions & 6 deletions x/perpetual/keeper/estimate_and_repay.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 17 additions & 27 deletions x/perpetual/keeper/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
))
}
32 changes: 32 additions & 0 deletions x/perpetual/keeper/force_close.go
Original file line number Diff line number Diff line change
@@ -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
}
36 changes: 0 additions & 36 deletions x/perpetual/keeper/force_close_long.go

This file was deleted.

Loading

0 comments on commit 19837d3

Please sign in to comment.