Skip to content

Commit

Permalink
feat(perp): distribute liquidate rewards keeper helper method and Liq…
Browse files Browse the repository at this point in the history
…uidationResp proto (#449)

* feat: liquidate proto changes, new params, and new methods on perp interfaces

* fix: restore passing state

* refactor: coalesce errors to one location

* feat (liquidate.go): ExecuteFullLiquidation, distributeLiquidateRewards

* test: Check expected fee to liquidator

* test (liquidate_test.go): turn tests green with expectedPerpEFBalance

* test: Test_distributeLiquidateRewards

* typo correction

* typo correction

* refactor: replace panic(err) with require.NoError

* fix: ExecuteFullLiquidation

* feat (perp): Emit internal events when positionResp objects are returned

* linter

* fix, test: perp.go and margin.go tests pass again

* fix: settleposition test restored

* fix: calc_test.go, calc_unit_test.go

* test: liquidate_unit_test passing

* fix, refactor: passing margin_test, liquidate_test

* fix (clearing_house_test.go): Margin and MarginToVault should be sdk.Int, not sdk.Dec

* test, docs (liquidate_test.go): Check correctness of emitted events. Add docs for calculations

* refactor: require.EqualValues -> assert.EqualValues + more docs

* docs: small decription

* refactor: universal sdk.Decs

* verify event calls

* refactor: consistency b/w assert and require

* refactor: rename CalcFee -> CalcPerpTxFee

* refactor: rename CalcFee -> CalcPerpTxFee

* refactor:  Liquidate (0/4) - asserts, String() calls, and new params

* refactor: clean up old TODOs in clearing_house.go

* feat: add liquidateresp as a proto type

* feat: Remove duplicate sdk.AccAddress transform

* Update x/perp/keeper/liquidate_test.go

Co-authored-by: Walter White <[email protected]>

* fix: added check to please linter

* Add distribute liquidate rewards

* Small refactoring

* Update state.proto

Co-authored-by: Unique-Divine <[email protected]>
Co-authored-by: AgentSmithMatrix <[email protected]>
Co-authored-by: MD <[email protected]>
Co-authored-by: Mat-Cosmos <[email protected]>
  • Loading branch information
5 people authored May 21, 2022
1 parent 2f34aa5 commit f04da51
Show file tree
Hide file tree
Showing 7 changed files with 758 additions and 91 deletions.
24 changes: 24 additions & 0 deletions proto/perp/v1/state.proto
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,30 @@ message PositionResp {
(gogoproto.nullable) = false];
}

message LiquidateResp{
// Amount of bad debt created by the liquidation event
string bad_debt = 1[
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false];

// Fee paid to the liquidator
string fee_to_liquidator = 2[
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false];

// Fee paid to the Perp EF fund
string fee_to_perp_ecosystem_fund = 3[
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false];

// Address of the liquidator
bytes liquidator = 4[
(gogoproto.casttype) = "github.com/cosmos/cosmos-sdk/types.AccAddress"];

// Position response from the close or open reverse position
PositionResp position_resp = 5;
}

message VirtualPoolInfo {
string pair = 1;
int64 last_restriction_block = 2;
Expand Down
2 changes: 0 additions & 2 deletions x/perp/keeper/clearing_house_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2013,7 +2013,6 @@ func TestDecreasePosition(t *testing.T) {
},

/*==========================SHORT POSITIONS===========================*/

{
name: "decrease short position, positive PnL",
// user bought in at 105 BTC for 10.5 NUSD at 10x leverage (1 BTC = 1 NUSD)
Expand Down Expand Up @@ -2094,7 +2093,6 @@ func TestDecreasePosition(t *testing.T) {
assert.EqualValues(t, ctx.BlockHeight(), resp.Position.BlockNumber)
},
},

{
name: "decrease short position, negative PnL",
// user bought in at 100 BTC for 10 NUSD at 10x leverage (1 BTC = 1 NUSD)
Expand Down
83 changes: 83 additions & 0 deletions x/perp/keeper/liquidate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package keeper

import (
sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/NibiruChain/nibiru/x/common"
"github.com/NibiruChain/nibiru/x/perp/events"
"github.com/NibiruChain/nibiru/x/perp/types"
)

func (k Keeper) distributeLiquidateRewards(
ctx sdk.Context, liquidateResp types.LiquidateResp) (err error) {
// --------------------------------------------------------------
// Preliminary validations
// --------------------------------------------------------------

// validate response
err = liquidateResp.Validate()
if err != nil {
return err
}

// validate pair
pair, err := common.NewTokenPairFromStr(liquidateResp.PositionResp.Position.Pair)
if err != nil {
return err
}
err = k.requireVpool(ctx, pair)
if err != nil {
return err
}

// --------------------------------------------------------------
// Distribution of rewards
// --------------------------------------------------------------

vaultAddr := k.AccountKeeper.GetModuleAddress(types.VaultModuleAccount)
perpEFAddr := k.AccountKeeper.GetModuleAddress(types.PerpEFModuleAccount)

// Transfer fee from vault to PerpEF
feeToPerpEF := liquidateResp.FeeToPerpEcosystemFund.RoundInt()
if feeToPerpEF.IsPositive() {
coinToPerpEF := sdk.NewCoin(
pair.GetQuoteTokenDenom(), feeToPerpEF)
err = k.BankKeeper.SendCoinsFromModuleToModule(
ctx,
/* from */ types.VaultModuleAccount,
/* to */ types.PerpEFModuleAccount,
sdk.NewCoins(coinToPerpEF),
)
if err != nil {
return err
}
events.EmitTransfer(ctx,
/* coin */ coinToPerpEF,
/* from */ vaultAddr.String(),
/* to */ perpEFAddr.String(),
)
}

// Transfer fee from PerpEF to liquidator
feeToLiquidator := liquidateResp.FeeToLiquidator.RoundInt()
if feeToLiquidator.IsPositive() {
coinToLiquidator := sdk.NewCoin(
pair.GetQuoteTokenDenom(), feeToLiquidator)
err = k.BankKeeper.SendCoinsFromModuleToAccount(
ctx,
/* from */ types.PerpEFModuleAccount,
/* to */ liquidateResp.Liquidator,
sdk.NewCoins(coinToLiquidator),
)
if err != nil {
return err
}
events.EmitTransfer(ctx,
/* coin */ coinToLiquidator,
/* from */ perpEFAddr.String(),
/* to */ liquidateResp.Liquidator.String(),
)
}

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

import (
"testing"

sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/NibiruChain/nibiru/x/common"
"github.com/NibiruChain/nibiru/x/perp/events"
"github.com/NibiruChain/nibiru/x/perp/types"

"github.com/NibiruChain/nibiru/x/testutil/sample"
)

func Test_distributeLiquidateRewards_Error(t *testing.T) {
testcases := []struct {
name string
test func()
}{
{
name: "empty LiquidateResponse fails validation - error",
test: func() {
perpKeeper, _, ctx := getKeeper(t)
err := perpKeeper.distributeLiquidateRewards(ctx,
types.LiquidateResp{})
require.Error(t, err)
require.ErrorContains(t, err, "must not have nil fields")
},
},
{
name: "invalid liquidator - panic",
test: func() {
perpKeeper, _, ctx := getKeeper(t)

require.Panics(t, func() {
err := perpKeeper.distributeLiquidateRewards(ctx,
types.LiquidateResp{BadDebt: sdk.OneDec(), FeeToLiquidator: sdk.OneDec(),
FeeToPerpEcosystemFund: sdk.OneDec(),
Liquidator: sdk.AccAddress{},
},
)
require.Error(t, err)
})
},
},
{
name: "invalid pair - error",
test: func() {
perpKeeper, _, ctx := getKeeper(t)
liquidator := sample.AccAddress()
err := perpKeeper.distributeLiquidateRewards(ctx,
types.LiquidateResp{BadDebt: sdk.OneDec(), FeeToLiquidator: sdk.OneDec(),
FeeToPerpEcosystemFund: sdk.OneDec(),
Liquidator: liquidator,
PositionResp: &types.PositionResp{
Position: &types.Position{
Pair: "dai:usdc:usdt",
}},
},
)
require.Error(t, err)
require.ErrorContains(t, err, common.ErrInvalidTokenPair.Error())
},
},
{
name: "vpool does not exist - error",
test: func() {
perpKeeper, mocks, ctx := getKeeper(t)
liquidator := sample.AccAddress()
pair := common.TokenPair("xxx:yyy")
mocks.mockVpoolKeeper.EXPECT().ExistsPool(ctx, pair).Return(false)
err := perpKeeper.distributeLiquidateRewards(ctx,
types.LiquidateResp{BadDebt: sdk.OneDec(), FeeToLiquidator: sdk.OneDec(),
FeeToPerpEcosystemFund: sdk.OneDec(),
Liquidator: liquidator,
PositionResp: &types.PositionResp{
Position: &types.Position{
Pair: pair.String(),
}},
},
)
require.Error(t, err)
require.ErrorContains(t, err, types.ErrPairNotFound.Error())
},
},
}

for _, tc := range testcases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
tc.test()
})
}
}

func Test_distributeLiquidateRewards_Happy(t *testing.T) {
testcases := []struct {
name string
test func()
}{
{
name: "healthy liquidation",
test: func() {
perpKeeper, mocks, ctx := getKeeper(t)
liquidator := sample.AccAddress()
pair := common.TokenPair("xxx:yyy")

mocks.mockVpoolKeeper.EXPECT().ExistsPool(ctx, pair).Return(true)

vaultAddr := authtypes.NewModuleAddress(types.VaultModuleAccount)
perpEFAddr := authtypes.NewModuleAddress(types.VaultModuleAccount)
mocks.mockAccountKeeper.EXPECT().GetModuleAddress(
types.VaultModuleAccount).
Return(vaultAddr)
mocks.mockAccountKeeper.EXPECT().GetModuleAddress(
types.PerpEFModuleAccount).
Return(perpEFAddr)

mocks.mockBankKeeper.EXPECT().SendCoinsFromModuleToModule(
ctx, types.VaultModuleAccount, types.PerpEFModuleAccount,
sdk.NewCoins(sdk.NewCoin("yyy", sdk.OneInt())),
).Return(nil)
mocks.mockBankKeeper.EXPECT().SendCoinsFromModuleToAccount(
ctx, types.PerpEFModuleAccount, liquidator,
sdk.NewCoins(sdk.NewCoin("yyy", sdk.OneInt())),
).Return(nil)

err := perpKeeper.distributeLiquidateRewards(ctx,
types.LiquidateResp{BadDebt: sdk.OneDec(), FeeToLiquidator: sdk.OneDec(),
FeeToPerpEcosystemFund: sdk.OneDec(),
Liquidator: liquidator,
PositionResp: &types.PositionResp{
Position: &types.Position{
Pair: pair.String(),
}},
},
)
require.NoError(t, err)

expectedEvents := []sdk.Event{
events.NewTransferEvent(
/* coin */ sdk.NewCoin("yyy", sdk.OneInt()),
/* from */ vaultAddr.String(),
/* to */ perpEFAddr.String(),
),
events.NewTransferEvent(
/* coin */ sdk.NewCoin("yyy", sdk.OneInt()),
/* from */ perpEFAddr.String(),
/* to */ liquidator.String(),
),
}
for _, event := range expectedEvents {
assert.Contains(t, ctx.EventManager().Events(), event)
}
},
},
}

for _, tc := range testcases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
tc.test()
})
}
}
12 changes: 4 additions & 8 deletions x/perp/types/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,23 +95,19 @@ func DefaultParams() Params {
}

func (p *Params) GetSpreadRatioAsDec() sdk.Dec {
return sdk.NewIntFromUint64(uint64(p.SpreadRatio)).
ToDec().Quo(sdk.MustNewDecFromStr("1000000"))
return sdk.NewDec(p.SpreadRatio).QuoInt64(1_000_000)
}

func (p *Params) GetTollRatioAsDec() sdk.Dec {
return sdk.NewIntFromUint64(uint64(p.TollRatio)).
ToDec().Quo(sdk.MustNewDecFromStr("1000000"))
return sdk.NewDec(p.TollRatio).QuoInt64(1_000_000)
}

func (p *Params) GetLiquidationFeeAsDec() sdk.Dec {
return sdk.NewIntFromUint64(uint64(p.LiquidationFee)).
ToDec().Quo(sdk.MustNewDecFromStr("1000000"))
return sdk.NewDec(p.LiquidationFee).QuoInt64(1_000_000)
}

func (p *Params) GetPartialLiquidationRatioAsDec() sdk.Dec {
return sdk.NewIntFromUint64(uint64(p.PartialLiquidationRatio)).
ToDec().Quo(sdk.MustNewDecFromStr("1000000"))
return sdk.NewDec(p.PartialLiquidationRatio).QuoInt64(1_000_000)
}

// Validate validates the set of params
Expand Down
Loading

0 comments on commit f04da51

Please sign in to comment.