Skip to content

Commit

Permalink
liquidation logic
Browse files Browse the repository at this point in the history
  • Loading branch information
hieuvubk committed Sep 22, 2024
1 parent c24aade commit 868dd11
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 91 deletions.
156 changes: 121 additions & 35 deletions x/vaults/keeper/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package keeper
import (
"context"
"fmt"
"sort"

"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
Expand Down Expand Up @@ -282,59 +283,80 @@ func (k *Keeper) ShouldLiquidate(
return false, nil
}

func (k *Keeper) GetLiquidateVaults(
func (k *Keeper) GetLiquidations(
ctx context.Context,
) ([]types.Vault, map[string]math.LegacyDec, error) {
var liquidateVaults []types.Vault
) ([]*types.Liquidation, error) {

liquidationRatios := make(map[string]math.LegacyDec)
prices := make(map[string]math.LegacyDec)
liquidations := make(map[string]*types.Liquidation)

err := k.VaultsManager.Walk(ctx, nil, func(key string, vm types.VaultMamager) (bool, error) {
price := k.GetPrice(ctx, vm.Denom)
prices[vm.Denom] = price
liquidationRatios[vm.Denom] = vm.Params.LiquidationRatio
liquidations[vm.Denom] = types.NewEmptyLiquidation(vm.Denom, price)

return false, nil
})
if err != nil {
return nil, nil, err
return nil, err
}

err = k.Vaults.Walk(ctx, nil, func(key uint64, vault types.Vault) (bool, error) {
err = k.Vaults.Walk(ctx, nil, func(id uint64, vault types.Vault) (bool, error) {
denom := vault.CollateralLocked.Denom
shouldLiquidate, err := k.ShouldLiquidate(ctx, vault, prices[denom], liquidationRatios[denom])
if shouldLiquidate && err != nil {
liquidateVaults = append(liquidateVaults, vault)
if shouldLiquidate && err == nil {
liquidations[denom].LiquidatingVaults = append(liquidations[denom].LiquidatingVaults, &vault)
liquidations[denom].VaultLiquidationStatus[id] = &types.VaultLiquidationStatus{}
}

return false, nil
})
if err != nil {
return nil, nil, err
return nil, err
}

return liquidateVaults, prices, nil
var result []*types.Liquidation
for _, liquidation := range liquidations {
if len(liquidation.LiquidatingVaults) != 0 {
result = append(result, liquidation)
}
}

return result, nil
}

func (k *Keeper) Liquidate(
ctx context.Context,
vault types.Vault,
sold sdk.Coin,
collateralRemain sdk.Coin,
liquidation types.Liquidation,
) error {
debt := vault.Debt.Amount
params := k.GetParams(ctx)

// Get total sold amount & collateral asset remain
var (
totalDebt, sold, totalCollateralRemain sdk.Coin
)

for _, vault := range liquidation.LiquidatingVaults {
totalDebt = totalDebt.Add(vault.Debt)
}

for _, status := range liquidation.VaultLiquidationStatus {
sold = sold.Add(status.Sold)
totalCollateralRemain = totalCollateralRemain.Add(status.RemainCollateral)
}

// Sold amount enough to cover debt
if sold.Amount.GTE(debt) {
if sold.Amount.GTE(totalDebt.Amount) {
// Burn debt
err := k.bankKeeper.BurnCoins(ctx, types.ModuleName, sdk.NewCoins(vault.Debt))
err := k.bankKeeper.BurnCoins(ctx, types.ModuleName, sdk.NewCoins(totalDebt))
if err != nil {
return err
}

// If remain sold, send to reserve
remain := sold.Sub(vault.Debt)
remain := sold.Sub(totalDebt)
if remain.Amount.GT(math.ZeroInt()) {
err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.ReserveModuleName, sdk.NewCoins(remain))
if err != nil {
Expand All @@ -343,29 +365,33 @@ func (k *Keeper) Liquidate(
}

// Take the liquidation penalty and send back to vault owner
if collateralRemain.Amount.GT(math.ZeroInt()) {
price := k.GetPrice(ctx, collateralRemain.Denom)
if totalCollateralRemain.Amount.GT(math.ZeroInt()) {
price := liquidation.MarkPrice
//TODO: decimal
penaltyAmount := math.LegacyNewDecFromInt(vault.Debt.Amount).Quo(price).Mul(params.LiquidationPenalty).TruncateInt()
if penaltyAmount.GTE(collateralRemain.Amount) {
err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.ReserveModuleName, sdk.NewCoins(collateralRemain))
if err != nil {
return err
}
return nil
} else {
err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.ReserveModuleName, sdk.NewCoins(sdk.NewCoin(collateralRemain.Denom, penaltyAmount)))
if err != nil {
return err

for _, vault := range liquidation.LiquidatingVaults {
collateralRemain := liquidation.VaultLiquidationStatus[vault.Id].RemainCollateral
if collateralRemain.Amount.Equal(math.ZeroInt()) {
continue
}
err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, sdk.MustAccAddressFromBech32(vault.Owner), sdk.NewCoins(sdk.NewCoin(collateralRemain.Denom, collateralRemain.Amount.Sub(penaltyAmount))))
if err != nil {
return err
penaltyAmount := math.LegacyNewDecFromInt(vault.Debt.Amount).Quo(price).Mul(params.LiquidationPenalty).TruncateInt()
if penaltyAmount.GTE(collateralRemain.Amount) {
err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.ReserveModuleName, sdk.NewCoins(collateralRemain))
if err != nil {
return err
}
} else {
err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.ReserveModuleName, sdk.NewCoins(sdk.NewCoin(collateralRemain.Denom, penaltyAmount)))
if err != nil {
return err
}
err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, sdk.MustAccAddressFromBech32(vault.Owner), sdk.NewCoins(sdk.NewCoin(collateralRemain.Denom, collateralRemain.Amount.Sub(penaltyAmount))))
if err != nil {
return err
}
}
return nil
}
}

} else {
// does not raise enough to cover nomUSD debt

Expand All @@ -376,13 +402,72 @@ func (k *Keeper) Liquidate(
}

// No collateral remain
if collateralRemain.Amount.Equal(math.ZeroInt()) {
if totalCollateralRemain.Amount.Equal(math.ZeroInt()) {
//TODO: send shortfall to reserve
return nil
} else {
// If there some collateral asset remain, try to reconstitue vault
// Priority by collateral ratio at momment
// So that mean we need less resource for high ratio vault


ratios := make([]math.LegacyDec, 0)
//TODO: Sort by CR in GetLiquidations could reduce calculate here
for _, vault := range liquidation.LiquidatingVaults {
penaltyAmount := math.LegacyNewDecFromInt(vault.Debt.Amount).Quo(liquidation.MarkPrice).Mul(params.LiquidationPenalty).TruncateInt()
err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.ReserveModuleName, sdk.NewCoins(sdk.NewCoin(liquidation.Denom, penaltyAmount)))
if err != nil {
return err
}
vault.CollateralLocked.Amount = vault.CollateralLocked.Amount.Sub(penaltyAmount)
totalCollateralRemain.Amount = totalCollateralRemain.Amount.Sub(penaltyAmount)

ratio := math.LegacyNewDecFromInt(vault.CollateralLocked.Amount).Mul(liquidation.MarkPrice).Quo(math.LegacyNewDecFromInt(vault.Debt.Amount))
ratios = append(ratios, ratio)
}

// Sort the vaults by CR in descending order
sort.Slice(liquidation.LiquidatingVaults, func(i, j int) bool {
return ratios[i].GT(ratios[j])
})

// Try to reconstitue vaults
totalRemainDebt := totalDebt.Sub(sold)
for _, vault := range liquidation.LiquidatingVaults {
// if remain debt & collateral can cover full vault
// open again
if vault.Debt.IsLTE(totalRemainDebt) && vault.CollateralLocked.IsLTE(totalCollateralRemain) {
totalRemainDebt = totalRemainDebt.Sub(vault.Debt)
totalCollateralRemain = totalCollateralRemain.Sub(vault.CollateralLocked)

vault.Status = types.ACTIVE
err := k.SetVault(ctx, *vault)
if err != nil {
return err
}
} else {
vault.Status = types.LIQUIDATED
err := k.SetVault(ctx, *vault)
if err != nil {
return err
}
}
}

// if remain collateral, send to reserve
if totalCollateralRemain.Amount.GT(math.ZeroInt()) {
err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.ReserveModuleName, sdk.NewCoins(totalCollateralRemain))
if err != nil {
return err
}
}

// if remain debt, send shortfall
// TODO: Shortfall

}
}
return nil
}

func (k *Keeper) GetVault(
Expand All @@ -404,6 +489,7 @@ func (k *Keeper) SetVault(
if err != nil {
return err
}
vault.Id = id

return k.Vaults.Set(ctx, id, vault)
}
112 changes: 56 additions & 56 deletions x/vaults/keeper/vaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,59 +282,59 @@ func (s *KeeperTestSuite) TestUpdateVaultsDebt() {
}
}

func (s *KeeperTestSuite) TestGetLiquidateVaults() {
s.SetupTest()
var (
denom1 = "atom"
denom2 = "osmo"
coin = sdk.NewCoin(denom1, math.NewInt(1000))
coinMintToAcc = sdk.NewCoin(denom1, math.NewInt(1000000))
maxDebt = math.NewInt(10000)
)

tests := []struct {
name string
setup func()
vaultId uint64
sender sdk.AccAddress
collateral sdk.Coin
}{
{
name: "success",
setup: func() {
err := s.k.ActiveCollateralAsset(s.Ctx, denom1, math.LegacyMustNewDecFromStr("0.1"), math.LegacyMustNewDecFromStr("0.1"), maxDebt)
s.Require().NoError(err)
err = s.k.ActiveCollateralAsset(s.Ctx, denom2, math.LegacyMustNewDecFromStr("0.1"), math.LegacyMustNewDecFromStr("0.1"), maxDebt)
s.Require().NoError(err)

vault := types.Vault{
Owner: s.TestAccs[0].String(),
Debt: sdk.NewCoin(denom1, maxDebt),
CollateralLocked: sdk.NewCoin(denom1, maxDebt),
Status: types.ACTIVE,
}
err = s.k.SetVault(s.Ctx, vault)
s.Require().NoError(err)

err = s.App.BankKeeper.MintCoins(s.Ctx, types.ModuleName, sdk.NewCoins(coinMintToAcc))
s.Require().NoError(err)
err = s.App.BankKeeper.SendCoinsFromModuleToAccount(s.Ctx, types.ModuleName, s.TestAccs[0], sdk.NewCoins(coinMintToAcc))
s.Require().NoError(err)
},
vaultId: 0,
sender: s.TestAccs[0],
collateral: coin,
},
}
for _, t := range tests {
s.Run(t.name, func() {
t.setup()
vaults, prices, err := s.k.GetLiquidateVaults(s.Ctx)
s.Require().NoError(err)

// current price = 1, vaults is empty,
s.Require().Equal(2, len(prices))
s.Require().Equal(0, len(vaults))
})
}
}
// func (s *KeeperTestSuite) TestGetLiquidateVaults() {
// s.SetupTest()
// var (
// denom1 = "atom"
// denom2 = "osmo"
// coin = sdk.NewCoin(denom1, math.NewInt(1000))
// coinMintToAcc = sdk.NewCoin(denom1, math.NewInt(1000000))
// maxDebt = math.NewInt(10000)
// )

// tests := []struct {
// name string
// setup func()
// vaultId uint64
// sender sdk.AccAddress
// collateral sdk.Coin
// }{
// {
// name: "success",
// setup: func() {
// err := s.k.ActiveCollateralAsset(s.Ctx, denom1, math.LegacyMustNewDecFromStr("0.1"), math.LegacyMustNewDecFromStr("0.1"), maxDebt)
// s.Require().NoError(err)
// err = s.k.ActiveCollateralAsset(s.Ctx, denom2, math.LegacyMustNewDecFromStr("0.1"), math.LegacyMustNewDecFromStr("0.1"), maxDebt)
// s.Require().NoError(err)

// vault := types.Vault{
// Owner: s.TestAccs[0].String(),
// Debt: sdk.NewCoin(denom1, maxDebt),
// CollateralLocked: sdk.NewCoin(denom1, maxDebt),
// Status: types.ACTIVE,
// }
// err = s.k.SetVault(s.Ctx, vault)
// s.Require().NoError(err)

// err = s.App.BankKeeper.MintCoins(s.Ctx, types.ModuleName, sdk.NewCoins(coinMintToAcc))
// s.Require().NoError(err)
// err = s.App.BankKeeper.SendCoinsFromModuleToAccount(s.Ctx, types.ModuleName, s.TestAccs[0], sdk.NewCoins(coinMintToAcc))
// s.Require().NoError(err)
// },
// vaultId: 0,
// sender: s.TestAccs[0],
// collateral: coin,
// },
// }
// for _, t := range tests {
// s.Run(t.name, func() {
// t.setup()
// vaults, prices, err := s.k.GetLiquidateVaults(s.Ctx)
// s.Require().NoError(err)

// // current price = 1, vaults is empty,
// s.Require().Equal(2, len(prices))
// s.Require().Equal(0, len(vaults))
// })
// }
// }
15 changes: 15 additions & 0 deletions x/vaults/types/liquidation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package types

import (
"cosmossdk.io/math"
// this line is used by starport scaffolding # 1
)

func NewEmptyLiquidation(denom string, price math.LegacyDec) *Liquidation {
return &Liquidation{
Denom: denom,
MarkPrice: price,
LiquidatingVaults: []*Vault{},
VaultLiquidationStatus: make(map[uint64]*VaultLiquidationStatus),
}
}

0 comments on commit 868dd11

Please sign in to comment.