Skip to content

Commit

Permalink
feat: funding fee payment flow (#261)
Browse files Browse the repository at this point in the history
* feat: funding fee payment flow

* test: fix tests

* test: fix

* test: fix
  • Loading branch information
cosmic-vagabond authored Nov 22, 2023
1 parent 420c7e9 commit 4cf6bef
Show file tree
Hide file tree
Showing 25 changed files with 1,868 additions and 167 deletions.
703 changes: 703 additions & 0 deletions docs/static/openapi.yml

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions proto/elys/margin/params.proto
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,17 @@ message Params {
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string funding_fee_base_rate = 22 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string funding_fee_max_rate = 23 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string funding_fee_min_rate = 24 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string funding_fee_collection_address = 25;
}
5 changes: 5 additions & 0 deletions proto/elys/margin/pool.proto
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,10 @@ message Pool {
repeated PoolAsset pool_assets_long = 6 [(gogoproto.nullable) = false];
repeated PoolAsset pool_assets_short = 7 [(gogoproto.nullable) = false];
int64 last_height_borrow_interest_rate_computed = 8;
// funding rate, if positive longs pay shorts, if negative shorts pay longs
string funding_rate = 9 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
}

14 changes: 14 additions & 0 deletions proto/elys/margin/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,20 @@ message MTP {
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
// funding fee paid
repeated cosmos.base.v1beta1.Coin funding_fee_paid_collaterals = 19 [
(gogoproto.nullable) = false
];
repeated cosmos.base.v1beta1.Coin funding_fee_paid_custodies = 20 [
(gogoproto.nullable) = false
];
// funding fee received
repeated cosmos.base.v1beta1.Coin funding_fee_received_collaterals = 21 [
(gogoproto.nullable) = false
];
repeated cosmos.base.v1beta1.Coin funding_fee_received_custodies = 22 [
(gogoproto.nullable) = false
];
}

message WhiteList {
Expand Down
4 changes: 4 additions & 0 deletions x/margin/client/cli/query_mtp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ func networkWithMTPObjects(t *testing.T, n int) (*network.Network, []*types.MTP)
SumCollateral: sdk.ZeroInt(),
TakeProfitPrice: sdk.MustNewDecFromStr(types.TakeProfitPriceDefault),
TakeProfitBorrowRate: sdk.OneDec(),
FundingFeePaidCollaterals: []sdk.Coin{sdk.NewCoin(paramtypes.BaseCurrency, sdk.NewInt(0))},
FundingFeePaidCustodies: []sdk.Coin{sdk.NewCoin("ATOM", sdk.NewInt(0))},
FundingFeeReceivedCollaterals: []sdk.Coin{sdk.NewCoin(paramtypes.BaseCurrency, sdk.NewInt(0))},
FundingFeeReceivedCustodies: []sdk.Coin{sdk.NewCoin("ATOM", sdk.NewInt(0))},
}

mtps = append(mtps, &mtp)
Expand Down
2 changes: 2 additions & 0 deletions x/margin/keeper/begin_blocker.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,14 @@ func (k Keeper) BeginBlocker(ctx sdk.Context) {
pool.BorrowInterestRate = rate
pool.LastHeightBorrowInterestRateComputed = currentHeight
_ = k.UpdatePoolHealth(ctx, &pool)
_ = k.UpdateFundingRate(ctx, &pool)
// TODO: function missing
// k.TrackSQBeginBlock(ctx, pool)
mtps, _, _ := k.GetMTPsForPool(ctx, pool.AmmPoolId, nil)
for _, mtp := range mtps {
BeginBlockerProcessMTP(ctx, k, mtp, pool, ammPool, baseCurrency)
}
_ = k.HandleFundingFeeDistribution(ctx, mtps, &pool, ammPool, baseCurrency)
}
k.SetPool(ctx, pool)
}
Expand Down
29 changes: 21 additions & 8 deletions x/margin/keeper/begin_blocker_process_mtp.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func BeginBlockerProcessMTP(ctx sdk.Context, k Keeper, mtp *types.MTP, pool type
for _, custody := range mtp.Custodies {
custodyAsset := custody.Denom
// Retrieve AmmPool
ammPool, err := k.CloseLongChecker.GetAmmPool(ctx, mtp.AmmPoolId, custodyAsset)
ammPool, err := k.GetAmmPool(ctx, mtp.AmmPoolId, custodyAsset)
if err != nil {
ctx.Logger().Error(errors.Wrap(err, fmt.Sprintf("error retrieving amm pool: %d", mtp.AmmPoolId)).Error())
return
Expand All @@ -53,33 +53,46 @@ func BeginBlockerProcessMTP(ctx sdk.Context, k Keeper, mtp *types.MTP, pool type
for _, collateral := range mtp.Collaterals {
collateralAsset := collateral.Denom
// Handle Borrow Interest if within epoch position
if err := k.CloseLongChecker.HandleBorrowInterest(ctx, mtp, &pool, ammPool, collateralAsset, custodyAsset); err != nil {
if err := k.HandleBorrowInterest(ctx, mtp, &pool, ammPool, collateralAsset, custodyAsset); err != nil {
ctx.Logger().Error(errors.Wrap(err, fmt.Sprintf("error handling borrow interest payment: %s", collateralAsset)).Error())
return
}
if err := k.HandleFundingFeeCollection(ctx, mtp, &pool, ammPool, collateralAsset, custodyAsset); err != nil {
ctx.Logger().Error(errors.Wrap(err, fmt.Sprintf("error handling funding fee collection: %s", collateralAsset)).Error())
return
}
}
}

_ = k.SetMTP(ctx, mtp)

var mustForceClose bool = false
for _, custody := range mtp.Custodies {
assetPrice, err := k.EstimateSwapGivenOut(ctx, sdk.NewCoin(custody.Denom, sdk.NewInt(1)), baseCurrency, ammPool)
assetPrice, err := k.EstimateSwap(ctx, sdk.NewCoin(custody.Denom, sdk.OneInt()), baseCurrency, ammPool)
if err != nil {
ctx.Logger().Error(errors.Wrap(err, fmt.Sprintf("error estimating swap given out: %s", custody.Denom)).Error())
return
ctx.Logger().Error(errors.Wrap(err, fmt.Sprintf("error estimating swap: %s", custody.Denom)).Error())
continue
}
if mtp.TakeProfitPrice.GT(sdk.NewDecFromInt(assetPrice)) {
// flag position as must force close
mustForceClose = true
break
}
ctx.Logger().Error(fmt.Sprintf("error executing force close on position %s because take profit price %s is less than asset price %s", mtp.String(), mtp.TakeProfitPrice.String(), sdk.NewDecFromInt(assetPrice).String()))
return
ctx.Logger().Error(fmt.Sprintf("skipping force close on position %s because take profit price %s is less than asset price %s", mtp.String(), mtp.TakeProfitPrice.String(), sdk.NewDecFromInt(assetPrice).String()))
}

// check MTP health against threshold
safetyFactor := k.GetSafetyFactor(ctx)

if mtp.MtpHealth.GT(safetyFactor) {
ctx.Logger().Error(errors.Wrap(types.ErrMTPHealthy, "error executing force close because mtp is healthy").Error())
ctx.Logger().Error(errors.Wrap(types.ErrMTPHealthy, "skipping executing force close because mtp is healthy").Error())
} else {
// flag position as must force close
mustForceClose = true
}

// if flag is false, then skip force close
if !mustForceClose {
return
}

Expand Down
2 changes: 1 addition & 1 deletion x/margin/keeper/calc_mtp_take_profit_liabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func (k Keeper) CalcMTPTakeProfitLiability(ctx sdk.Context, mtp *types.MTP, base
}

// convert custody amount to base currency
C, err := k.EstimateSwapGivenOut(ctx, takeProfitCustody, baseCurrency, ammPool)
C, err := k.EstimateSwap(ctx, takeProfitCustody, baseCurrency, ammPool)
if err != nil {
return sdk.ZeroInt(), err
}
Expand Down
21 changes: 14 additions & 7 deletions x/margin/keeper/events.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package keeper

import (
"fmt"
"strconv"

sdk "github.com/cosmos/cosmos-sdk/types"
Expand All @@ -20,16 +21,22 @@ func (k Keeper) EmitForceClose(ctx sdk.Context, mtp *types.MTP, repayAmount sdk.
sdk.NewAttribute("id", strconv.FormatInt(int64(mtp.Id), 10)),
sdk.NewAttribute("position", mtp.Position.String()),
sdk.NewAttribute("address", mtp.Address),
// sdk.NewAttribute("collateral_asset", mtp.CollateralAsset),
// sdk.NewAttribute("collateral_amount", mtp.CollateralAmount.String()),
// sdk.NewAttribute("custody_asset", mtp.CustodyAsset),
// sdk.NewAttribute("custody_amount", mtp.CustodyAmount.String()),
sdk.NewAttribute("collaterals", fmt.Sprintf("%s", mtp.Collaterals)),
sdk.NewAttribute("custodies", fmt.Sprintf("%s", mtp.Custodies)),
sdk.NewAttribute("repay_amount", repayAmount.String()),
// sdk.NewAttribute("leverage", mtp.Leverage.String()),
sdk.NewAttribute("leverage", fmt.Sprintf("%s", mtp.Leverages)),
sdk.NewAttribute("liabilities", mtp.Liabilities.String()),
// sdk.NewAttribute("borrow_interest_paid_collateral", mtp.BorrowInterestPaidCollateral.String()),
// sdk.NewAttribute("borrow_interest_paid_custody", mtp.BorrowInterestPaidCustody.String()),
sdk.NewAttribute("borrow_interest_paid_collaterals", fmt.Sprintf("%s", mtp.BorrowInterestPaidCollaterals)),
sdk.NewAttribute("borrow_interest_paid_custodies", fmt.Sprintf("%s", mtp.BorrowInterestPaidCustodies)),
sdk.NewAttribute("health", mtp.MtpHealth.String()),
sdk.NewAttribute("closer", closer),
))
}

func (k Keeper) EmitFundingFeePayment(ctx sdk.Context, mtp *types.MTP, takeAmount sdk.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),
))
}
2 changes: 1 addition & 1 deletion x/margin/keeper/handle_borrow_interest_payment.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ func (k Keeper) HandleBorrowInterestPayment(ctx sdk.Context, collateralAsset str
incrementalBorrowInterestPaymentEnabled := k.GetIncrementalBorrowInterestPaymentEnabled(ctx)
// if incremental payment on, pay interest
if incrementalBorrowInterestPaymentEnabled {
finalBorrowInterestPayment, err := k.IncrementalBorrowInterestPayment(ctx, collateralAsset, custodyAsset, borrowInterestPayment, mtp, pool, ammPool)
finalBorrowInterestPayment, err := k.IncrementalBorrowInterestPayment(ctx, collateralAsset, custodyAsset, borrowInterestPayment, mtp, pool, ammPool, baseCurrency)
if err != nil {
ctx.Logger().Error(sdkerrors.Wrap(err, "error executing incremental borrow interest payment").Error())
} else {
Expand Down
96 changes: 96 additions & 0 deletions x/margin/keeper/handle_funding_fee_collection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package keeper

import (
sdk "github.com/cosmos/cosmos-sdk/types"
ammtypes "github.com/elys-network/elys/x/amm/types"
"github.com/elys-network/elys/x/margin/types"
)

// HandleFundingFeeCollection handles funding fee collection
func (k Keeper) HandleFundingFeeCollection(ctx sdk.Context, mtp *types.MTP, pool *types.Pool, ammPool ammtypes.Pool, collateralAsset string, custodyAsset string) error {
// get funding rate
fundingRate := pool.FundingRate

// if funding rate is zero, return
if fundingRate.IsZero() {
return nil
}

// if funding rate is negative and mtp position is long or funding rate is positive and mtp position is short, return
if (fundingRate.IsNegative() && mtp.Position == types.Position_LONG) || (fundingRate.IsPositive() && mtp.Position == types.Position_SHORT) {
return nil
}

// get indexes
collateralIndex, custodyIndex := types.GetMTPAssetIndex(mtp, collateralAsset, custodyAsset)

// Calculate the take amount in custody asset
takeAmountCustody := types.CalcTakeAmount(mtp.Custodies[custodyIndex], custodyAsset, fundingRate)

// Swap the take amount to collateral asset
takeAmountCollateralAmount, err := k.EstimateSwap(ctx, takeAmountCustody, collateralAsset, ammPool)
if err != nil {
return err
}

// Create the take amount coin
takeAmountCollateral := sdk.NewCoin(collateralAsset, takeAmountCollateralAmount)

// Get funding fee collection address
fundingFeeCollectionAddress := k.GetFundingFeeCollectionAddress(ctx)

// Transfer take amount in collateral asset to funding fee collection address
_, err = k.TakeFundPayment(ctx, takeAmountCollateral.Amount, collateralAsset, sdk.OneDec(), fundingFeeCollectionAddress, &ammPool)
if err != nil {
return err
}

// update mtp custody
mtp.Custodies[custodyIndex] = mtp.Custodies[custodyIndex].Sub(takeAmountCustody)

// add payment to total funding fee paid in collateral asset
mtp.FundingFeePaidCollaterals[collateralIndex] = mtp.FundingFeePaidCollaterals[collateralIndex].Add(takeAmountCollateral)

// add payment to total funding fee paid in custody asset
mtp.FundingFeePaidCustodies[custodyIndex] = mtp.FundingFeePaidCustodies[custodyIndex].Add(takeAmountCustody)

// emit event
if !takeAmountCollateral.IsZero() {
k.EmitFundingFeePayment(ctx, mtp, takeAmountCustody.Amount, collateralAsset, types.EventIncrementalPayFund)
}

// update pool custody balance
err = pool.UpdateCustody(ctx, custodyAsset, takeAmountCustody.Amount, false, mtp.Position)
if err != nil {
return err
}

// update accounted balance for custody side
err = pool.UpdateBalance(ctx, custodyAsset, takeAmountCustody.Amount, false, mtp.Position)
if err != nil {
return err
}

// update accounted balance for collateral side
err = pool.UpdateBalance(ctx, collateralAsset, takeAmountCollateral.Amount, false, mtp.Position)
if err != nil {
return err
}

// apply changes to mtp object
err = k.SetMTP(ctx, mtp)
if err != nil {
return err
}

// apply changes to pool object
k.SetPool(ctx, *pool)

// update mtp health
_, err = k.UpdateMTPHealth(ctx, *mtp, ammPool, custodyAsset)
if err != nil {
return err
}

return nil
}
98 changes: 98 additions & 0 deletions x/margin/keeper/handle_funding_fee_distribution.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package keeper

import (
sdk "github.com/cosmos/cosmos-sdk/types"
ammtypes "github.com/elys-network/elys/x/amm/types"
"github.com/elys-network/elys/x/margin/types"
)

// HandleFundingFeeDistribution handles funding fee distribution
func (k Keeper) HandleFundingFeeDistribution(ctx sdk.Context, mtps []*types.MTP, pool *types.Pool, ammPool ammtypes.Pool, baseCurrency string) error {
// get funding rate
fundingRate := pool.FundingRate

// if funding rate is zero, return
if fundingRate.IsZero() {
return nil
}

// account liabilities from long position
liabilitiesLong := sdk.ZeroInt()
for _, asset := range pool.PoolAssetsLong {
liabilitiesLong = liabilitiesLong.Add(asset.Liabilities)
}

// account liabilities from short position
liabilitiesShort := sdk.ZeroInt()
for _, asset := range pool.PoolAssetsShort {
liabilitiesShort = liabilitiesShort.Add(asset.Liabilities)
}

// get funding fee collection address
fundingFeeCollectionAddress := k.GetFundingFeeCollectionAddress(ctx)

// get base currency balance
balance := k.bankKeeper.GetBalance(ctx, fundingFeeCollectionAddress, baseCurrency)

// if balance is zero, return
if balance.IsZero() {
return nil
}

for _, mtp := range mtps {
// if funding rate is negative and mtp position is short or funding rate is positive and mtp position is long, return
if (fundingRate.IsNegative() && mtp.Position == types.Position_SHORT) || (fundingRate.IsPositive() && mtp.Position == types.Position_LONG) {
return nil
}

// get mtp address
mtpAddress, err := sdk.AccAddressFromBech32(mtp.Address)
if err != nil {
return err
}

// calc funding fee share
fundingFeeShare := sdk.ZeroDec()
if fundingRate.IsNegative() && mtp.Position == types.Position_LONG {
fundingFeeShare = sdk.NewDecFromInt(mtp.Liabilities).Quo(sdk.NewDecFromInt(liabilitiesLong))
}
if fundingRate.IsPositive() && mtp.Position == types.Position_SHORT {
fundingFeeShare = sdk.NewDecFromInt(mtp.Liabilities).Quo(sdk.NewDecFromInt(liabilitiesShort))
}

// if funding fee share is zero, skip mtp
if fundingFeeShare.IsZero() {
continue
}

// calculate funding fee amount
fundingFeeAmount := sdk.NewCoin(baseCurrency, sdk.NewDecFromInt(balance.Amount).Mul(fundingFeeShare).TruncateInt())

// transfer funding fee amount to mtp address
if err := k.bankKeeper.SendCoins(ctx, fundingFeeCollectionAddress, mtpAddress, sdk.NewCoins(fundingFeeAmount)); err != nil {
return err
}

// update received funding fee accounting buckets
for custodyIndex, _ := range mtp.Custodies {
for collateralIndex, collateral := range mtp.Collaterals {
// Swap the take amount to collateral asset
fundingFeeCollateralAmount, err := k.EstimateSwap(ctx, fundingFeeAmount, collateral.Denom, ammPool)
if err != nil {
return err
}

// Create the take amount coin
fundingFeeCollateral := sdk.NewCoin(collateral.Denom, fundingFeeCollateralAmount)

// add payment to total funding fee paid in collateral asset
mtp.FundingFeeReceivedCollaterals[collateralIndex] = mtp.FundingFeePaidCollaterals[collateralIndex].Add(fundingFeeCollateral)

// add payment to total funding fee paid in custody asset
mtp.FundingFeeReceivedCustodies[custodyIndex] = mtp.FundingFeePaidCustodies[custodyIndex].Add(fundingFeeAmount)
}
}
}

return nil
}
Loading

0 comments on commit 4cf6bef

Please sign in to comment.