Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 185 additions & 21 deletions api/elys/perpetual/params.pulsar.go

Large diffs are not rendered by default.

742 changes: 411 additions & 331 deletions api/elys/perpetual/query.pulsar.go

Large diffs are not rendered by default.

199 changes: 133 additions & 66 deletions api/elys/perpetual/types.pulsar.go

Large diffs are not rendered by default.

43 changes: 36 additions & 7 deletions app/setup_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package app

import (
"context"
"cosmossdk.io/math"
"fmt"
oracletypes "github.com/elys-network/elys/v7/x/oracle/types"
"strings"

storetypes "cosmossdk.io/store/types"
Expand Down Expand Up @@ -62,13 +64,40 @@ func (app *ElysApp) setUpgradeHandler() {

vm, vmErr := app.mm.RunMigrations(ctx, app.configurator, vm)

//oracleParams := app.OracleKeeper.GetParams(ctx)
//if len(oracleParams.MandatoryList) == 0 {
// err := app.ojoOracleMigration(ctx, plan.Height+1)
// if err != nil {
// return nil, err
// }
//}
if ctx.ChainID() == "elys-1" {
newPriceFeeders := []sdk.AccAddress{
sdk.MustAccAddressFromBech32("elys1y5jeztqtf7vwqe6wd7tv2z8mzjg38hn6zkpmvq"),
sdk.MustAccAddressFromBech32("elys16r4y6hdehvntgg70avg6f0s2x55k3wekeyw8vw"),
}

for _, accAddress := range newPriceFeeders {
v := oracletypes.PriceFeeder{
Feeder: accAddress.String(),
IsActive: true,
}
app.OracleKeeper.SetPriceFeeder(ctx, v)
}
}

receiver := sdk.MustAccAddressFromBech32("elys1kxgan4uq0m8gqztd09n6qm627p6v4ayzngxyx8")
sender := sdk.MustAccAddressFromBech32("elys1p2fhrn9zfra9lv5062nvzkmp9hduhm9hkk6kz6a3ucwktjvuzv9smry5sk")
amount := sdk.NewCoins(
sdk.NewInt64Coin("ibc/F082B65C88E4B6D5EF1DB243CDA1D331D002759E938A0F5CD3FFDC5D53B3E349", 85_000_000), //USDC
sdk.NewInt64Coin("ibc/C4CFF46FD6DE35CA4CF4CE031E643C8FDC9BA4B99AE598E9B0ED98FE3A2319F9", 1_036_566_310), // ATOM
sdk.NewInt64Coin("ibc/8BFE59DCD5A7054F0A97CF91F3E3ABCA8C5BA454E548FA512B729D4004584D47", 3_000_000), // NTRN
sdk.NewCoin("ibc/8464A63954C0350A26C8588E20719F3A0AC8705E4CA0F7450B60C3F16B2D3421", math.LegacyMustNewDecFromStr("10000000000000000000").TruncateInt()), // XRP
sdk.NewCoin("ibc/ADF401C952ADD9EE232D52C8303B8BE17FE7953C8D420F20769AF77240BD0C58", math.LegacyMustNewDecFromStr("51059039885106925").TruncateInt()), // INJ
sdk.NewInt64Coin("ibc/45D6B52CAD911A15BD9C2F5FFDA80E26AFCB05C7CD520070790ABC86D2B24229", 9_960_000), // TIA
)
if ctx.ChainID() == "elysicstestnet-1" {
sender = sdk.MustAccAddressFromBech32("elys1jeqlq99ustyug8rxgsrtmf3awzl83x535v3svykfwkkkr7049wxqgdt6ss")
amount = sdk.NewCoins(sdk.NewInt64Coin("ibc/C4CFF46FD6DE35CA4CF4CE031E643C8FDC9BA4B99AE598E9B0ED98FE3A2319F9", 100000000), sdk.NewInt64Coin("uelys", 1000000000))
}
cacheCtx, write := ctx.CacheContext()
err := app.BankKeeper.SendCoins(cacheCtx, sender, receiver, amount)
if err == nil {
write()
}

return vm, vmErr
},
Expand Down
10 changes: 10 additions & 0 deletions proto/elys/perpetual/params.proto
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,14 @@ message Params {
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];
string second_liquidation_trigger_ratio = 26 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];
string first_liquidation_closing_ratio = 27 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];
}
5 changes: 5 additions & 0 deletions proto/elys/perpetual/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,11 @@ message PoolResponse {
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];
string mtp_safety_factor = 16 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];
}

message QueryCloseEstimationRequest {
Expand Down
1 change: 1 addition & 0 deletions proto/elys/perpetual/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ message MTP {
uint64 last_interest_calc_block = 23;
uint64 last_funding_calc_time = 24;
uint64 last_funding_calc_block = 25;
bool partial_liquidation_done = 26;
}

message InterestBlock {
Expand Down
98 changes: 98 additions & 0 deletions x/perpetual/keeper/add_collateral.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package keeper

import (
errorsmod "cosmossdk.io/errors"
"cosmossdk.io/math"
"errors"
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
ammtypes "github.com/elys-network/elys/v7/x/amm/types"
assetprofiletypes "github.com/elys-network/elys/v7/x/assetprofile/types"
ptypes "github.com/elys-network/elys/v7/x/parameter/types"
"github.com/elys-network/elys/v7/x/perpetual/types"
)

func (k Keeper) AddCollateral(ctx sdk.Context, mtp *types.MTP, pool *types.Pool, collateral sdk.Coin, ammPool *ammtypes.Pool) (sdk.Coin, error) {
entry, found := k.assetProfileKeeper.GetEntry(ctx, ptypes.BaseCurrency)
if !found {
return sdk.Coin{}, errorsmod.Wrapf(assetprofiletypes.ErrAssetProfileNotFound, "asset %s not found", ptypes.BaseCurrency)
}
baseCurrency := entry.Denom

if mtp.Position == types.Position_LONG && mtp.CollateralAsset == baseCurrency {
if collateral.Denom != mtp.CollateralAsset {
return sdk.Coin{}, errors.New("denom not same as collateral asset")
}

if collateral.Denom != mtp.LiabilitiesAsset {
return sdk.Coin{}, errors.New("denom not same as liabilities asset")
}

creator := mtp.GetAccountAddress()

// interest amount has been paid from custody
params := k.GetParams(ctx)
maxAmount := mtp.Liabilities.Sub(params.LongMinimumLiabilityAmount)
if !maxAmount.IsPositive() {
return sdk.Coin{}, fmt.Errorf("cannot reduce liabilties less than %s", params.LongMinimumLiabilityAmount.String())
}

var finalAmount math.Int
if collateral.Amount.LT(maxAmount) {
finalAmount = collateral.Amount
} else {
finalAmount = maxAmount
}

mtp.Liabilities = mtp.Liabilities.Sub(finalAmount)
err := pool.UpdateLiabilities(mtp.LiabilitiesAsset, finalAmount, false, mtp.Position)
if err != nil {
return sdk.Coin{}, err
}

mtp.Collateral = mtp.Collateral.Add(finalAmount)
err = pool.UpdateCollateral(mtp.CollateralAsset, finalAmount, true, mtp.Position)
if err != nil {
return sdk.Coin{}, err
}

finalCollateralCoin := sdk.NewCoin(collateral.Denom, finalAmount)
err = k.SendToAmmPool(ctx, creator, ammPool, sdk.NewCoins(finalCollateralCoin))
if err != nil {
return sdk.Coin{}, err
}

err = k.SetMTP(ctx, mtp)
if err != nil {
return sdk.Coin{}, err
}
k.SetPool(ctx, *pool)

if k.hooks != nil {
err = k.hooks.AfterPerpetualPositionModified(ctx, *ammPool, *pool, creator)
if err != nil {
return sdk.Coin{}, err
}
}
return finalCollateralCoin, nil

} else {
msgOpen := types.MsgOpen{
Creator: mtp.Address,
Position: mtp.Position,
Leverage: math.LegacyZeroDec(),
Collateral: collateral,
TakeProfitPrice: math.LegacyZeroDec(),
StopLossPrice: math.LegacyZeroDec(),
PoolId: mtp.AmmPoolId,
}
if err := msgOpen.ValidateBasic(); err != nil {
return sdk.Coin{}, err
}
_, err := k.Open(ctx, &msgOpen)
if err != nil {
return sdk.Coin{}, err
}
return sdk.Coin{}, nil
}
}
10 changes: 5 additions & 5 deletions x/perpetual/keeper/close_position.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,17 @@ func (k Keeper) ClosePosition(ctx sdk.Context, msg *types.MsgClose) (types.MTP,
}

// this also handles edge case where bot is unable to close position in time.
repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, allInterestsPaid, forceClosed, perpetualFeesCoins, closingPrice, err := k.MTPTriggerChecksAndUpdates(ctx, &mtp, &pool, &ammPool)
repayAmt, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, allInterestsPaid, forceClosed, perpetualFeesCoins, closingPrice, closingRatio, err := k.MTPTriggerChecksAndUpdates(ctx, &mtp, &pool, &ammPool)
if err != nil {
return types.MTP{}, math.ZeroInt(), math.LegacyZeroDec(), math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), false, false, zeroPerpFees, math.LegacyZeroDec(), initialCollateral, initialCustody, initialLiabilities, err
}

if forceClosed {
return mtp, repayAmt, math.LegacyOneDec(), returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, allInterestsPaid, forceClosed, perpetualFeesCoins, closingPrice, initialCollateral, initialCustody, initialLiabilities, nil
return mtp, repayAmt, closingRatio, returnAmt, fundingFeeAmt, fundingAmtDistributed, interestAmt, insuranceAmt, allInterestsPaid, forceClosed, perpetualFeesCoins, closingPrice, initialCollateral, initialCustody, initialLiabilities, nil
}

// Should be declared after SettleMTPBorrowInterestUnpaidLiability and settling funding
closingRatio := msg.Amount.ToLegacyDec().Quo(mtp.Custody.ToLegacyDec())
// Should be reset after MTPTriggerChecksAndUpdates
closingRatio = msg.Amount.ToLegacyDec().Quo(mtp.Custody.ToLegacyDec())
if mtp.Position == types.Position_SHORT {
closingRatio = msg.Amount.ToLegacyDec().Quo(mtp.Liabilities.ToLegacyDec())
}
Expand All @@ -53,7 +53,7 @@ func (k Keeper) ClosePosition(ctx sdk.Context, msg *types.MsgClose) (types.MTP,
}

// Estimate swap and repay
repayAmt, returnAmt, perpFees, closingPrice, err := k.EstimateAndRepay(ctx, &mtp, &pool, &ammPool, closingRatio)
repayAmt, returnAmt, perpFees, closingPrice, _, err := k.EstimateAndRepay(ctx, &mtp, &pool, &ammPool, closingRatio, false)
if err != nil {
return mtp, math.ZeroInt(), math.LegacyZeroDec(), math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), math.ZeroInt(), allInterestsPaid, forceClosed, perpetualFeesCoins, math.LegacyZeroDec(), initialCollateral, initialCustody, initialLiabilities, err
}
Expand Down
80 changes: 0 additions & 80 deletions x/perpetual/keeper/close_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,86 +136,6 @@ func (suite *PerpetualKeeperTestSuite) TestClose() {
"",
math.NewInt(30), // less than at the same price
},
{
"Close at take profit price",
func() *types.MsgClose {
suite.ResetSuite()

addr := suite.AddAccounts(1, nil)
positionCreator := addr[0]
_, _, ammPool := suite.SetPerpetualPool(1)
tradingAssetPrice, _, err := suite.app.PerpetualKeeper.GetAssetPriceAndAssetUsdcDenomRatio(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,
Collateral: sdk.NewCoin(ptypes.BaseCurrency, math.NewInt(1000)),
TakeProfitPrice: tradingAssetPrice.MulInt64(4),
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: tradingAssetPrice.MulInt64(4),
Provider: oracleProvider.String(),
Timestamp: uint64(suite.ctx.BlockTime().Unix()),
})

return &types.MsgClose{
Creator: positionCreator.String(),
Id: position.Id,
Amount: math.NewInt(699),
PoolId: ammPool.PoolId,
}
},
"",
math.NewInt(91),
},
{
"Close at stopLoss price",
func() *types.MsgClose {
suite.ResetSuite()

addr := suite.AddAccounts(1, nil)
positionCreator := addr[0]
_, _, ammPool := suite.SetPerpetualPool(1)
tradingAssetPrice, _, err := suite.app.PerpetualKeeper.GetAssetPriceAndAssetUsdcDenomRatio(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,
Collateral: sdk.NewCoin(ptypes.BaseCurrency, math.NewInt(1000)),
TakeProfitPrice: tradingAssetPrice.MulInt64(4),
StopLossPrice: math.LegacyMustNewDecFromStr("2.0"),
}

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("2.0"),
Provider: oracleProvider.String(),
Timestamp: uint64(suite.ctx.BlockTime().Unix()),
})

return &types.MsgClose{
Creator: positionCreator.String(),
Id: position.Id,
Amount: math.NewInt(699),
PoolId: ammPool.PoolId,
}
},
"",
math.NewInt(501),
},
{
"Success: close long position,at same price as open price",
func() *types.MsgClose {
Expand Down
32 changes: 14 additions & 18 deletions x/perpetual/keeper/estimate_and_repay.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,24 @@ package keeper
import (
"fmt"

errorsmod "cosmossdk.io/errors"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
ammtypes "github.com/elys-network/elys/v7/x/amm/types"
assetprofiletypes "github.com/elys-network/elys/v7/x/assetprofile/types"
ptypes "github.com/elys-network/elys/v7/x/parameter/types"
"github.com/elys-network/elys/v7/x/perpetual/types"
"github.com/osmosis-labs/osmosis/osmomath"
)

// 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, closingRatio math.LegacyDec) (math.Int, math.Int, types.PerpetualFees, math.LegacyDec, error) {
func (k Keeper) EstimateAndRepay(ctx sdk.Context, mtp *types.MTP, pool *types.Pool, ammPool *ammtypes.Pool, closingRatio math.LegacyDec, isLiquidation bool) (math.Int, math.Int, types.PerpetualFees, math.LegacyDec, sdk.Coin, error) {

if closingRatio.LTE(math.LegacyZeroDec()) || closingRatio.GT(math.LegacyOneDec()) {
return math.Int{}, math.Int{}, types.PerpetualFees{}, math.LegacyZeroDec(), fmt.Errorf("invalid closing ratio (%s)", closingRatio.String())
return math.Int{}, math.Int{}, types.PerpetualFees{}, math.LegacyZeroDec(), sdk.Coin{}, fmt.Errorf("invalid closing ratio (%s)", closingRatio.String())
}
zeroPerpFees := types.NewPerpetualFeesWithEmptyCoins()
repayAmount, payingLiabilities, _, slippageAmount, weightBreakingFee, repayOracleAmount, perpetualFees, takerFees, closingPrice, err := k.CalcRepayAmount(ctx, mtp, ammPool, closingRatio)
if err != nil {
return math.ZeroInt(), math.ZeroInt(), zeroPerpFees, math.LegacyZeroDec(), err
return math.ZeroInt(), math.ZeroInt(), zeroPerpFees, math.LegacyZeroDec(), sdk.Coin{}, err
}
perpFees := k.CalculatePerpetualFees(ctx, ammPool.PoolParams.UseOracle, sdk.NewCoin(mtp.CustodyAsset, repayAmount), sdk.NewCoin(mtp.LiabilitiesAsset, payingLiabilities), slippageAmount, weightBreakingFee, perpetualFees, takerFees, repayOracleAmount, false, false)
// Track slippage and weight breaking fee slippage in amm via perpetual
Expand All @@ -39,21 +36,20 @@ func (k Keeper) EstimateAndRepay(ctx sdk.Context, mtp *types.MTP, pool *types.Po

returnAmount, err := k.CalcReturnAmount(*mtp, repayAmount, closingRatio)
if err != nil {
return math.ZeroInt(), math.ZeroInt(), zeroPerpFees, math.LegacyZeroDec(), err
return math.ZeroInt(), math.ZeroInt(), zeroPerpFees, math.LegacyZeroDec(), sdk.Coin{}, err
}

entry, found := k.assetProfileKeeper.GetEntry(ctx, ptypes.BaseCurrency)
if !found {
return math.Int{}, math.Int{}, types.PerpetualFees{}, math.LegacyZeroDec(), errorsmod.Wrapf(assetprofiletypes.ErrAssetProfileNotFound, "asset %s not found", ptypes.BaseCurrency)
_, tradingAssetDenomPrice, err := k.GetAssetPriceAndAssetUsdcDenomRatio(ctx, mtp.TradingAsset)
if err != nil {
return math.Int{}, math.Int{}, types.PerpetualFees{}, math.LegacyZeroDec(), sdk.Coin{}, err
}
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, &perpFees, repayAmount); err != nil {
return math.ZeroInt(), math.ZeroInt(), zeroPerpFees, math.LegacyZeroDec(), err
collateralToAdd, err := k.Repay(ctx, mtp, pool, ammPool, returnAmount, payingLiabilities, closingRatio, &perpFees, repayAmount, isLiquidation, tradingAssetDenomPrice)
if err != nil {
return math.ZeroInt(), math.ZeroInt(), zeroPerpFees, math.LegacyZeroDec(), sdk.Coin{}, err
}

return repayAmount, returnAmount, perpFees, closingPrice, nil
return repayAmount, returnAmount, perpFees, closingPrice, collateralToAdd, nil
}

// CalcRepayAmount repay amount is in custody asset for liabilities with closing ratio
Expand Down Expand Up @@ -116,10 +112,10 @@ func (k Keeper) CalcReturnAmount(mtp types.MTP, repayAmount math.Int, closingRat

// returnAmount is in custody asset
// closingCollatoralCoin is the collateral coin before closing position
func (k Keeper) CalcNetPnLAtClosing(ctx sdk.Context, returnAmount math.Int, custodyAsset string, closingCollatoralCoin sdk.Coin, closingRatio math.LegacyDec) (netPnLInUSD math.LegacyDec) {
func (k Keeper) CalcNetPnLAtClosing(ctx sdk.Context, returnAmount math.Int, custodyAsset string, closingCollateralCoin sdk.Coin, closingRatio math.LegacyDec) (netPnLInUSD math.LegacyDec) {

closedCollateralAmount := closingRatio.MulInt(closingCollatoralCoin.Amount).TruncateInt()
closedCollateralInUSD := k.amm.CalculateUSDValue(ctx, closingCollatoralCoin.Denom, closedCollateralAmount).Dec()
closedCollateralAmount := closingRatio.MulInt(closingCollateralCoin.Amount).TruncateInt()
closedCollateralInUSD := k.amm.CalculateUSDValue(ctx, closingCollateralCoin.Denom, closedCollateralAmount).Dec()

// returnAmount will always be >=0
returnAmountInUSD := k.amm.CalculateUSDValue(ctx, custodyAsset, returnAmount).Dec()
Expand Down
Loading
Loading