Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(perp): ClosePosition with bad debt #1493

Merged
merged 26 commits into from
Jul 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0d0f869
refactor: clean code
k-yang Jul 5, 2023
d180b34
refactor: clean code
k-yang Jul 5, 2023
5b924e9
refactor: clean code
k-yang Jul 5, 2023
7d52341
refactor: clean code
k-yang Jul 5, 2023
47416e3
test(perp): fix RemoveMargin test
k-yang Jul 5, 2023
60f817b
Merge branch 'fix/perp/event-assertion' into fix/perp/close-position-…
k-yang Jul 5, 2023
c68a504
fix(perp): allow closing positions with bad debt
k-yang Jul 5, 2023
a6adf49
test(perp): close position tests
k-yang Jul 5, 2023
d1806a5
test(perp): close short position
k-yang Jul 5, 2023
aec551e
test(perp): close long position with bad debt
k-yang Jul 5, 2023
24d6924
chore: remove redundant ClosePosition tests
k-yang Jul 5, 2023
8cbc145
Merge branch 'master' into fix/perp/close-position-with-bad-debt
k-yang Jul 5, 2023
f8d3fa0
Update CHANGELOG.md
k-yang Jul 5, 2023
8a24c81
refactor: clean code
k-yang Jul 5, 2023
86e368f
fix(perp): add bad debt check on partial close
k-yang Jul 5, 2023
503c872
refactor: add context to error messages
k-yang Jul 5, 2023
8d77f34
test(perp): partial close below maintenance margin ratio
k-yang Jul 5, 2023
045a586
Merge branch 'master' into fix/perp/close-position-with-bad-debt
k-yang Jul 5, 2023
6ecceb6
test(perp); partial close below mmr
k-yang Jul 5, 2023
a2d74b7
test(perp); partial close with bad debt
k-yang Jul 5, 2023
eeeedc6
test(perp): partial close short position with bad debt
k-yang Jul 5, 2023
afcc3ba
refactor: clean code
k-yang Jul 5, 2023
e8ae92a
fix: use updated AMM when calculating the margin ratio
k-yang Jul 5, 2023
a93926d
Merge branch 'master' into fix/perp/close-position-with-bad-debt
k-yang Jul 6, 2023
c234cd6
linter
k-yang Jul 6, 2023
05934b7
Merge branch 'master' into fix/perp/close-position-with-bad-debt
Unique-Divine Jul 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Improvements

* #[1466](https://github.com/NibiruChain/nibiru/pull/1466) - refactor(perp): `PositionLiquidatedEvent`
* #[1494](https://github.com/NibiruChain/nibiru/pull/1494) - feat: create cli to add sudo account into genesis
* [#1466](https://github.com/NibiruChain/nibiru/pull/1466) - refactor(perp): `PositionLiquidatedEvent`
* [#1494](https://github.com/NibiruChain/nibiru/pull/1494) - feat: create cli to add sudo account into genesis
* [#1493](https://github.com/NibiruChain/nibiru/pull/1493) - fix(perp): allow `ClosePosition` when there is bad debt

### Features

Expand Down
4 changes: 2 additions & 2 deletions x/perp/v2/integration/action/market.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ func WithSqrtDepth(amount sdk.Dec) marketModifier {
}
}

func WithMarketLatestCPF(cpf sdk.Dec) marketModifier {
func WithLatestMarketCPF(amount sdk.Dec) marketModifier {
return func(market *types.Market, amm *types.AMM) {
market.LatestCumulativePremiumFraction = cpf
market.LatestCumulativePremiumFraction = amount
}
}

Expand Down
4 changes: 2 additions & 2 deletions x/perp/v2/integration/action/position.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,10 +353,10 @@ func (p partialCloseFails) Do(app *app.NibiruApp, ctx sdk.Context) (sdk.Context,
_, err := app.PerpKeeperV2.PartialClose(ctx, p.pair, p.trader, p.amount)

if !errors.Is(err, p.expectedErr) {
return ctx, fmt.Errorf("expected error %s, got %s", p.expectedErr, err), true
return ctx, fmt.Errorf("expected error %s, got %s", p.expectedErr, err), false
}

return ctx, nil, true
return ctx, nil, false
}

func PartialCloseFails(trader sdk.AccAddress, pair asset.Pair, amount sdk.Dec, expecedErr error) action.Action {
Expand Down
15 changes: 9 additions & 6 deletions x/perp/v2/integration/assertion/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ func (act containsLiquidateEvent) Do(_ *app.NibiruApp, ctx sdk.Context) (
outCtx sdk.Context, err error, isMandatory bool,
) {
foundEvent := false
events := ctx.EventManager().Events()
matchingEvents := []abci.Event{}
for _, sdkEvent := range events {

for _, sdkEvent := range ctx.EventManager().Events() {
if sdkEvent.Type != proto.MessageName(act.expectedEvent) {
continue
}
Expand Down Expand Up @@ -74,7 +74,7 @@ func (act containsLiquidateEvent) Do(_ *app.NibiruApp, ctx sdk.Context) (
return ctx, errors.New(
strings.Join([]string{
fmt.Sprintf("expected: %+v.", sdk.StringifyEvents([]abci.Event{abci.Event(expected)})),
fmt.Sprintf("found %v events:", len(events)),
fmt.Sprintf("found %v events:", len(ctx.EventManager().Events())),
fmt.Sprintf("events of matching type:\n%v", sdk.StringifyEvents(matchingEvents).String()),
}, "\n"),
), false
Expand All @@ -99,10 +99,12 @@ func (p positionChangedEventShouldBeEqual) Do(_ *app.NibiruApp, ctx sdk.Context)
if sdkEvent.Type != proto.MessageName(p.expectedEvent) {
continue
}

abciEvent := abci.Event{
Type: sdkEvent.Type,
Attributes: sdkEvent.Attributes,
}

typedEvent, err := sdk.ParseTypedEvent(abciEvent)
if err != nil {
return ctx, err, false
Expand All @@ -119,16 +121,17 @@ func (p positionChangedEventShouldBeEqual) Do(_ *app.NibiruApp, ctx sdk.Context)

if !reflect.DeepEqual(p.expectedEvent, positionChangedEvent) {
expected, _ := sdk.TypedEventToEvent(p.expectedEvent)
actual, _ := sdk.TypedEventToEvent(positionChangedEvent)
return ctx, fmt.Errorf(`expected event is not equal to the actual event.
want:
%+v
got:
%+v`, sdk.StringifyEvents([]abci.Event{abci.Event(expected)}), sdk.StringifyEvents([]abci.Event{abci.Event(actual)})), false
%+v`, sdk.StringifyEvents([]abci.Event{abci.Event(expected)}), sdk.StringifyEvents([]abci.Event{abciEvent})), false
}

return ctx, nil, false
}

return ctx, nil, false
return ctx, fmt.Errorf("unable to find desired event of type %s", proto.MessageName(p.expectedEvent)), false
}

// PositionChangedEventShouldBeEqual checks that the position changed event is
Expand Down
2 changes: 1 addition & 1 deletion x/perp/v2/integration/assertion/position.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ type positionShouldNotExist struct {
func (p positionShouldNotExist) Do(app *app.NibiruApp, ctx sdk.Context) (sdk.Context, error, bool) {
_, err := app.PerpKeeperV2.Positions.Get(ctx, collections.Join(p.Pair, p.Account))
if err == nil {
return ctx, fmt.Errorf("position should not exist"), false
return ctx, fmt.Errorf("position should not exist, but it does with pair %s", p.Pair), false
}

return ctx, nil, false
Expand Down
91 changes: 49 additions & 42 deletions x/perp/v2/keeper/clearing_house.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,16 @@ func (k Keeper) MarketOrder(
) (positionResp *types.PositionResp, err error) {
market, err := k.Markets.Get(ctx, pair)
if err != nil {
return nil, fmt.Errorf("%w: %s", types.ErrPairNotFound, pair)
return nil, types.ErrPairNotFound.Wrapf("pair %s not found", pair)
}

if !market.Enabled {
return nil, fmt.Errorf("%w: %s", types.ErrMarketNotEnabled, pair)
return nil, types.ErrMarketNotEnabled.Wrapf("market pair %s not enabled", pair)
}

amm, err := k.AMMs.Get(ctx, pair)
if err != nil {
return nil, fmt.Errorf("%w: %s", types.ErrPairNotFound, pair)
return nil, types.ErrPairNotFound.Wrapf("pair %s not found", pair)
}

err = checkMarketOrderRequirements(market, quoteAssetAmt, leverage)
Expand Down Expand Up @@ -97,6 +97,18 @@ func (k Keeper) MarketOrder(
}
}

// check bad debt
if !positionResp.BadDebt.IsZero() {
return nil, types.ErrBadDebt.Wrapf("bad debt %s", positionResp.BadDebt)
}

if !positionResp.Position.Size_.IsZero() {
err = k.checkMarginRatio(ctx, market, *updatedAMM, positionResp.Position)
if err != nil {
return nil, err
}
}

if err = k.afterPositionUpdate(
ctx, market, *updatedAMM, traderAddr, *positionResp, types.ChangeReason_MarketOrder,
); err != nil {
Expand Down Expand Up @@ -144,10 +156,20 @@ func (k Keeper) increasePosition(
baseAmtLimit sdk.Dec, // unsigned
leverage sdk.Dec, // unsigned
) (updatedAMM *types.AMM, positionResp *types.PositionResp, err error) {
positionResp = &types.PositionResp{}
marginIncrease := increasedNotional.Quo(leverage)
fundingPayment := FundingPayment(currentPosition, market.LatestCumulativePremiumFraction) // signed
remainingMargin := currentPosition.Margin.Add(marginIncrease).Sub(fundingPayment) // signed
positionNotional, err := PositionNotionalSpot(amm, currentPosition)
if err != nil {
return nil, nil, err
}

positionResp = &types.PositionResp{
RealizedPnl: sdk.ZeroDec(),
MarginToVault: increasedNotional.Quo(leverage), // unsigned
FundingPayment: FundingPayment(currentPosition, market.LatestCumulativePremiumFraction), // signed
ExchangedNotionalValue: increasedNotional, // unsigned
PositionNotional: positionNotional.Add(increasedNotional), // unsigned
}

remainingMargin := currentPosition.Margin.Add(positionResp.MarginToVault).Sub(positionResp.FundingPayment) // signed

updatedAMM, baseAssetDeltaAbs, err := k.SwapQuoteAsset(
ctx,
Expand All @@ -167,16 +189,6 @@ func (k Keeper) increasePosition(
positionResp.ExchangedPositionSize = baseAssetDeltaAbs.Neg()
}

positionNotional, err := PositionNotionalSpot(amm, currentPosition)
if err != nil {
return nil, nil, err
}

positionResp.ExchangedNotionalValue = increasedNotional
positionResp.PositionNotional = positionNotional.Add(increasedNotional)
positionResp.RealizedPnl = sdk.ZeroDec()
positionResp.MarginToVault = marginIncrease
positionResp.FundingPayment = fundingPayment
positionResp.BadDebt = sdk.MinDec(sdk.ZeroDec(), remainingMargin).Abs()
positionResp.Position = types.Position{
TraderAddress: currentPosition.TraderAddress,
Expand Down Expand Up @@ -539,18 +551,6 @@ func (k Keeper) afterPositionUpdate(
positionResp types.PositionResp,
changeType types.ChangeReason,
) (err error) {
// check bad debt
if !positionResp.BadDebt.IsZero() {
return fmt.Errorf("bad debt must be zero to prevent attacker from leveraging it")
}

if !positionResp.Position.Size_.IsZero() {
err = k.checkMarginRatio(ctx, market, amm, positionResp.Position)
if err != nil {
return err
}
}

// transfer trader <=> vault
marginToVault := positionResp.MarginToVault.RoundInt()
switch {
Expand All @@ -566,7 +566,9 @@ func (k Keeper) afterPositionUpdate(
}
}

transferredFee, err := k.transferFee(ctx, market.Pair, traderAddr, positionResp.ExchangedNotionalValue)
transferredFee, err := k.transferFee(ctx, market.Pair, traderAddr, positionResp.ExchangedNotionalValue,
market.ExchangeFeeRatio, market.EcosystemFundFeeRatio,
)
if err != nil {
return err
}
Expand Down Expand Up @@ -642,13 +644,10 @@ func (k Keeper) transferFee(
pair asset.Pair,
trader sdk.AccAddress,
positionNotional sdk.Dec,
exchangeFeeRatio sdk.Dec,
ecosystemFundFeeRatio sdk.Dec,
) (fees sdkmath.Int, err error) {
m, err := k.Markets.Get(ctx, pair)
if err != nil {
return sdkmath.Int{}, err
}

feeToExchangeFeePool := m.ExchangeFeeRatio.Mul(positionNotional).RoundInt()
feeToExchangeFeePool := exchangeFeeRatio.Mul(positionNotional).RoundInt()
if feeToExchangeFeePool.IsPositive() {
if err = k.BankKeeper.SendCoinsFromAccountToModule(
ctx,
Expand All @@ -665,7 +664,7 @@ func (k Keeper) transferFee(
}
}

feeToEcosystemFund := m.EcosystemFundFeeRatio.Mul(positionNotional).RoundInt()
feeToEcosystemFund := ecosystemFundFeeRatio.Mul(positionNotional).RoundInt()
if feeToEcosystemFund.IsPositive() {
if err = k.BankKeeper.SendCoinsFromAccountToModule(
ctx,
Expand Down Expand Up @@ -731,8 +730,6 @@ func (k Keeper) ClosePosition(ctx sdk.Context, pair asset.Pair, traderAddr sdk.A
); err != nil {
return nil, err
}

return positionResp, nil
}

if err = k.afterPositionUpdate(
Expand Down Expand Up @@ -843,16 +840,16 @@ func (k Keeper) PartialClose(
ctx sdk.Context,
pair asset.Pair,
traderAddr sdk.AccAddress,
sizeAmt sdk.Dec,
sizeAmt sdk.Dec, //unsigned
) (*types.PositionResp, error) {
market, err := k.Markets.Get(ctx, pair)
if err != nil {
return nil, fmt.Errorf("%w: %s", types.ErrPairNotFound, pair)
return nil, types.ErrPairNotFound.Wrapf("pair: %s", pair)
}

amm, err := k.AMMs.Get(ctx, pair)
if err != nil {
return nil, fmt.Errorf("%w: %s", types.ErrPairNotFound, pair)
return nil, types.ErrPairNotFound.Wrapf("pair: %s", pair)
}

position, err := k.Positions.Get(ctx, collections.Join(pair, traderAddr))
Expand Down Expand Up @@ -886,6 +883,16 @@ func (k Keeper) PartialClose(
return nil, err
}

if positionResp.BadDebt.IsPositive() {
if err = k.realizeBadDebt(
ctx,
market,
positionResp.BadDebt.RoundInt(),
); err != nil {
return nil, err
}
}

err = k.afterPositionUpdate(
ctx,
market,
Expand Down
Loading
Loading