Skip to content

Commit

Permalink
[LeverageLp]: Update liquidation algorithm (#646)
Browse files Browse the repository at this point in the history
* optimise liquidation

* store sorted key with consistent length

* changes

* consider old debt

* del

* add sorted list test

* add interest hook

* stablestake hooks

* fix

* remove

* add collateral, close, add lev tests

* fix

* add hook in app.go

* add hook function test

* add migration

* liquidate
  • Loading branch information
amityadav0 authored Jul 15, 2024
1 parent d3a457a commit f44b203
Show file tree
Hide file tree
Showing 21 changed files with 442 additions and 37 deletions.
1 change: 1 addition & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,7 @@ func NewElysApp(
app.StablestakeKeeper = *app.StablestakeKeeper.SetHooks(stablestakekeeper.NewMultiStableStakeHooks(
app.MasterchefKeeper.StableStakeHooks(),
app.TierKeeper.StableStakeHooks(),
app.LeveragelpKeeper.StableStakeHooks(),
))
stablestakeModule := stablestake.NewAppModule(appCodec, app.StablestakeKeeper, app.AccountKeeper, app.BankKeeper)

Expand Down
2 changes: 1 addition & 1 deletion x/leveragelp/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState)

// Set all the pool
for _, elem := range genState.PositionList {
k.SetPosition(ctx, &elem)
k.SetPosition(ctx, &elem, sdk.NewInt(0))
}

// Set genesis Position count
Expand Down
3 changes: 2 additions & 1 deletion x/leveragelp/keeper/add_collateral.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func (k Keeper) ProcessAddCollateral(ctx sdk.Context, address string, id uint64,
return errorsmod.Wrap(types.ErrPositionDisabled, fmt.Sprintf("poolId: %d", position.AmmPoolId))
}

oldDebt := k.stableKeeper.GetDebt(ctx, position.GetPositionAddress())
// Check if collateral is not more than borrowed
debtBefore := k.stableKeeper.UpdateInterestStackedByAddress(ctx, position.GetPositionAddress())
maxAllowedCollateral := debtBefore.Borrowed.Add(debtBefore.InterestStacked).Sub(debtBefore.InterestPaid)
Expand Down Expand Up @@ -65,7 +66,7 @@ func (k Keeper) ProcessAddCollateral(ctx sdk.Context, address string, id uint64,
position.Liabilities = debt.Borrowed
position.Collateral = position.Collateral.Add(sdk.NewCoin(position.Collateral.Denom, collateral))

k.SetPosition(ctx, &position)
k.SetPosition(ctx, &position, oldDebt.Borrowed.Add(oldDebt.InterestStacked).Sub(oldDebt.InterestPaid))

if k.hooks != nil {
k.hooks.AfterLeveragelpPositionModified(ctx, ammPool, pool)
Expand Down
19 changes: 7 additions & 12 deletions x/leveragelp/keeper/begin_blocker.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,11 @@ func (k Keeper) BeginBlocker(ctx sdk.Context) {
continue
}
if k.IsPoolEnabled(ctx, pool.AmmPoolId) {
positions, _, _ := k.GetPositionsForPool(ctx, pool.AmmPoolId, nil)
for _, position := range positions {
k.LiquidatePositionIfUnhealthy(ctx, position, pool, ammPool)
}

// Liquidate positions liquidation health threshold
// Design
// - `Health = PositionValue / Debt`, PositionValue is based on LpToken price change
// - Debt growth speed is relying on debt.Borrowed.
// - Things are sorted by `LeveragedLpAmount / debt.Borrowed` per pool to liquidate efficiently

// TODO: should consider InterestStacked-InterestPaid amount
// - `Health = PositionValue / liability`, PositionValue is based on LpToken price change
// - Debt growth speed is relying on liability.
// - Things are sorted by `LeveragedLpAmount / liability` per pool to liquidate efficiently
k.IteratePoolPosIdsLiquidationSorted(ctx, pool.AmmPoolId, func(posId types.AddressId) bool {
position, err := k.GetPosition(ctx, posId.Address, posId.Id)
if err != nil {
Expand Down Expand Up @@ -80,8 +73,9 @@ func (k Keeper) LiquidatePositionIfUnhealthy(ctx sdk.Context, position *types.Po
ctx.Logger().Error(errors.Wrap(err, fmt.Sprintf("error updating position health: %s", position.String())).Error())
return false, true
}
debt := k.stableKeeper.GetDebt(ctx, position.GetPositionAddress())
position.PositionHealth = h
k.SetPosition(ctx, position)
k.SetPosition(ctx, position, debt.Borrowed.Add(debt.InterestStacked).Sub(debt.InterestPaid))

params := k.GetParams(ctx)
isHealthy = position.PositionHealth.GT(params.SafetyFactor)
Expand Down Expand Up @@ -119,8 +113,9 @@ func (k Keeper) ClosePositionIfUnderStopLossPrice(ctx sdk.Context, position *typ
ctx.Logger().Error(errors.Wrap(err, fmt.Sprintf("error updating position health: %s", position.String())).Error())
return false, true
}
debt := k.stableKeeper.GetDebt(ctx, position.GetPositionAddress())
position.PositionHealth = h
k.SetPosition(ctx, position)
k.SetPosition(ctx, position, debt.Borrowed.Add(debt.InterestStacked).Sub(debt.InterestPaid))

lpTokenPrice, err := ammPool.LpTokenPrice(ctx, k.oracleKeeper)
if err != nil {
Expand Down
144 changes: 143 additions & 1 deletion x/leveragelp/keeper/begin_blocker_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package keeper_test

import (
"fmt"
"time"

"cosmossdk.io/math"
"github.com/cometbft/cometbft/crypto/ed25519"
sdk "github.com/cosmos/cosmos-sdk/types"
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
"github.com/elys-network/elys/x/leveragelp/types"
stablestakekeeper "github.com/elys-network/elys/x/stablestake/keeper"
stablestaketypes "github.com/elys-network/elys/x/stablestake/types"
)

func (suite KeeperTestSuite) TestBeginBlocker() {
Expand Down Expand Up @@ -64,10 +69,147 @@ func (suite KeeperTestSuite) TestLiquidatePositionIfUnhealthy() {

cacheCtx, _ = suite.ctx.CacheContext()
position.StopLossPrice = math.LegacyNewDec(100000)
k.SetPosition(cacheCtx, position)
k.SetPosition(cacheCtx, position, sdk.NewInt(0))
underStopLossPrice, earlyReturn := k.ClosePositionIfUnderStopLossPrice(cacheCtx, position, pool, ammPool)
suite.Require().True(underStopLossPrice)
suite.Require().False(earlyReturn)
_, err = k.GetPosition(cacheCtx, position.Address, position.Id)
suite.Require().Error(err)
}

func (suite KeeperTestSuite) TestLiquidatePositionSorted() {
k := suite.app.LeveragelpKeeper
addr := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address())
position, _ := suite.OpenPosition(addr)

// open positions with other addresses
addr2 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address())
addr3 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address())
addr4 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address())
addr5 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address())

usdcTokenTotal := sdk.NewInt64Coin("uusdc", 500000)
usdcToken := sdk.NewInt64Coin("uusdc", 100000)
err := suite.app.BankKeeper.MintCoins(suite.ctx, minttypes.ModuleName, sdk.Coins{usdcTokenTotal})
suite.Require().NoError(err)
err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, minttypes.ModuleName, addr2, sdk.Coins{usdcToken})
suite.Require().NoError(err)
err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, minttypes.ModuleName, addr3, sdk.Coins{usdcToken})
suite.Require().NoError(err)
err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, minttypes.ModuleName, addr4, sdk.Coins{usdcToken})
suite.Require().NoError(err)
err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, minttypes.ModuleName, addr5, sdk.Coins{usdcToken})
suite.Require().NoError(err)

stableMsgServer := stablestakekeeper.NewMsgServerImpl(suite.app.StablestakeKeeper)
_, err = stableMsgServer.Bond(sdk.WrapSDKContext(suite.ctx), &stablestaketypes.MsgBond{
Creator: addr2.String(),
Amount: sdk.NewInt(100000),
})
suite.Require().NoError(err)

position3, err := k.OpenLong(suite.ctx, &types.MsgOpen{
Creator: addr3.String(),
CollateralAsset: "uusdc",
CollateralAmount: sdk.NewInt(2000),
AmmPoolId: 1,
Leverage: sdk.NewDec(2),
})
suite.Require().NoError(err)

position4, err := k.OpenLong(suite.ctx, &types.MsgOpen{
Creator: addr4.String(),
CollateralAsset: "uusdc",
CollateralAmount: sdk.NewInt(2000),
AmmPoolId: 1,
Leverage: sdk.NewDec(6),
})
suite.Require().NoError(err)

position5, err := k.OpenLong(suite.ctx, &types.MsgOpen{
Creator: addr5.String(),
CollateralAsset: "uusdc",
CollateralAmount: sdk.NewInt(2000),
AmmPoolId: 1,
Leverage: sdk.NewDec(4),
})
suite.Require().NoError(err)

ammPool, found := suite.app.AmmKeeper.GetPool(suite.ctx, position3.AmmPoolId)
suite.Require().True(found)
health, err := k.GetPositionHealth(suite.ctx, *position3, ammPool)
suite.Require().NoError(err)
suite.Require().Equal(health.String(), "2.000000000000000000") // slippage disabled on amm

health, err = k.GetPositionHealth(suite.ctx, *position, ammPool)
suite.Require().NoError(err)
suite.Require().Equal(health.String(), "1.250000000000000000") // slippage disabled on amm

health, err = k.GetPositionHealth(suite.ctx, *position4, ammPool)
suite.Require().NoError(err)
suite.Require().Equal(health.String(), "1.200000000000000000") // slippage disabled on amm

health, err = k.GetPositionHealth(suite.ctx, *position5, ammPool)
suite.Require().NoError(err)
suite.Require().Equal(health.String(), "1.333333333333333333") // slippage disabled on amm

// Check order in list
suite.app.LeveragelpKeeper.IteratePoolPosIdsLiquidationSorted(suite.ctx, position.AmmPoolId, func(posId types.AddressId) bool {
position, _ := k.GetPosition(suite.ctx, posId.Address, posId.Id)
health, _ := k.GetPositionHealth(suite.ctx, position, ammPool)
fmt.Printf("Address: %s, Id: %d, value: %s\n", position.Address, position.Id, health.String())
return false
})

err = k.ProcessAddCollateral(suite.ctx, addr4.String(), position4.Id, sdk.NewInt(1000))
suite.Require().NoError(err)

// Check order in list
suite.app.LeveragelpKeeper.IteratePoolPosIdsLiquidationSorted(suite.ctx, position.AmmPoolId, func(posId types.AddressId) bool {
position, _ := k.GetPosition(suite.ctx, posId.Address, posId.Id)
health, _ := k.GetPositionHealth(suite.ctx, position, ammPool)
fmt.Printf("Address: %s, Id: %d, value: %s\n", position.Address, position.Id, health.String())
return false
})

// add more lev
k.OpenConsolidate(suite.ctx, position5, &types.MsgOpen{
Creator: addr5.String(),
CollateralAsset: "uusdc",
CollateralAmount: sdk.NewInt(1000),
AmmPoolId: 1,
Leverage: sdk.NewDec(4),
})
suite.Require().NoError(err)

// Check order in list
suite.app.LeveragelpKeeper.IteratePoolPosIdsLiquidationSorted(suite.ctx, position.AmmPoolId, func(posId types.AddressId) bool {
position, _ := k.GetPosition(suite.ctx, posId.Address, posId.Id)
health, _ := k.GetPositionHealth(suite.ctx, position, ammPool)
fmt.Printf("Address: %s, Id: %d, value: %s\n", position.Address, position.Id, health.String())
return false
})

// Partial close.
var (
msg = &types.MsgClose{
Creator: addr5.String(),
Id: position5.Id,
LpAmount: position5.LeveragedLpAmount.Quo(sdk.NewInt(2)),
}
)
suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour))

_, _, err = k.CloseLong(suite.ctx, msg)
suite.Require().NoError(err)

// Check order in list
suite.app.LeveragelpKeeper.IteratePoolPosIdsLiquidationSorted(suite.ctx, position.AmmPoolId, func(posId types.AddressId) bool {
position, _ := k.GetPosition(suite.ctx, posId.Address, posId.Id)
health, _ := k.GetPositionHealth(suite.ctx, position, ammPool)
fmt.Printf("Address: %s, Id: %d, value: %s\n", position.Address, position.Id, health.String())
return false
})
}

// Add stablestake update hook test
37 changes: 37 additions & 0 deletions x/leveragelp/keeper/hooks_stablestake.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package keeper

import (
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
stablestaketypes "github.com/elys-network/elys/x/stablestake/types"
)

func (k Keeper) AfterUpdateInterestStacked(ctx sdk.Context, address string, old sdk.Int, new sdk.Int) error {
k.SetSortedLiquidation(ctx, address, old, new)
return nil
}

// Hooks wrapper struct for incentive keeper
type StableStakeHooks struct {
k Keeper
}

var _ stablestaketypes.StableStakeHooks = StableStakeHooks{}

// Return the wrapper struct
func (k Keeper) StableStakeHooks() StableStakeHooks {
return StableStakeHooks{k}
}

func (h StableStakeHooks) AfterBond(ctx sdk.Context, sender string, shareAmount math.Int) error {
return nil
}

func (h StableStakeHooks) AfterUnbond(ctx sdk.Context, sender string, shareAmount math.Int) error {
return nil
}

func (h StableStakeHooks) AfterUpdateInterestStacked(ctx sdk.Context, address string, old sdk.Int, new sdk.Int) error {
h.k.AfterUpdateInterestStacked(ctx, address, old, new)
return nil
}
3 changes: 2 additions & 1 deletion x/leveragelp/keeper/msg_server_update_stop_loss.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ func (k msgServer) UpdateStopLoss(goCtx context.Context, msg *types.MsgUpdateSto
return nil, err
}

debt := k.stableKeeper.GetDebt(ctx, position.GetPositionAddress())
position.StopLossPrice = msg.Price
k.SetPosition(ctx, position)
k.SetPosition(ctx, position, debt.Borrowed.Add(debt.InterestStacked).Sub(debt.InterestPaid))

event := sdk.NewEvent(types.EventOpen,
sdk.NewAttribute("id", strconv.FormatInt(int64(position.Id), 10)),
Expand Down
Loading

0 comments on commit f44b203

Please sign in to comment.