diff --git a/x/vaults/keeper/vault.go b/x/vaults/keeper/vault.go index d0d1aac..325722f 100644 --- a/x/vaults/keeper/vault.go +++ b/x/vaults/keeper/vault.go @@ -3,6 +3,7 @@ package keeper import ( "context" "fmt" + "sort" "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" @@ -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 { @@ -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 @@ -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( @@ -404,6 +489,7 @@ func (k *Keeper) SetVault( if err != nil { return err } + vault.Id = id return k.Vaults.Set(ctx, id, vault) } diff --git a/x/vaults/keeper/vaults_test.go b/x/vaults/keeper/vaults_test.go index 1142551..39764c7 100644 --- a/x/vaults/keeper/vaults_test.go +++ b/x/vaults/keeper/vaults_test.go @@ -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)) +// }) +// } +// } diff --git a/x/vaults/types/liquidation.go b/x/vaults/types/liquidation.go new file mode 100644 index 0000000..2a6cbb6 --- /dev/null +++ b/x/vaults/types/liquidation.go @@ -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), + } +}