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

fix(perp): add pair to liquidation failed events #1462

Merged
merged 9 commits into from
Jun 30, 2023
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Improvements

* [#1462](https://github.com/NibiruChain/nibiru/pull/1462) - fix(perp): Add pair to liquidation failed event.
* [#1424](https://github.com/NibiruChain/nibiru/pull/1424) - feat(perp): Add change type and exchanged margin to position changed events.
* [#1390](https://github.com/NibiruChain/nibiru/pull/1390) - fix(localnet.sh): Fix genesis market initialization + add force exits on failure
* [#1340](https://github.com/NibiruChain/nibiru/pull/1340) - feat(wasm): Enforce x/sudo contract permission checks on the shifter contract + integration tests
Expand Down Expand Up @@ -598,4 +599,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Testing

* [#695](https://github.com/NibiruChain/nibiru/pull/695) Add `OpenPosition` integration tests.
* [#692](https://github.com/NibiruChain/nibiru/pull/692) Add test coverage for Perp MsgServer methods.
* [#692](https://github.com/NibiruChain/nibiru/pull/692) Add test coverage for Perp MsgServer methods.
7 changes: 6 additions & 1 deletion proto/nibiru/perp/v2/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ message MsgMultiLiquidateResponse {
// nullable since no fee is taken on failed liquidation

string trader = 5;
string pair = 6 [
(gogoproto.customtype) =
"github.com/NibiruChain/nibiru/x/common/asset.Pair",
(gogoproto.nullable) = false
];
}

repeated LiquidationResponse liquidations = 1;
Expand Down Expand Up @@ -266,4 +271,4 @@ message MsgDonateToEcosystemFund {
];
}

message MsgDonateToEcosystemFundResponse {}
message MsgDonateToEcosystemFundResponse {}
38 changes: 38 additions & 0 deletions x/common/testutil/events.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package testutil

import (
"fmt"
"reflect"
"strings"

"encoding/json"

"github.com/cosmos/gogoproto/proto"

"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"

abci "github.com/cometbft/cometbft/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -59,3 +66,34 @@ func RequireContainsTypedEvent(t require.TestingT, ctx sdk.Context, event proto.

t.Errorf("event not found, event: %+v, found events: %+v", event, foundEvents)
}

// ProtoToJson converts a proto message into a JSON string using the proto codec.
// A codec defines a functionality for serializing other objects. The proto
// codec provides full Protobuf serialization compatibility.
func ProtoToJson(protoMsg proto.Message) (jsonOut string, err error) {
protoCodec := codec.NewProtoCodec(codectypes.NewInterfaceRegistry())
var jsonBz json.RawMessage
jsonBz, err = protoCodec.MarshalJSON(protoMsg)
return string(jsonBz), err
}

// EventHasAttribueValue parses the given ABCI event at a key to see if it
// matches (contains) the wanted value.
//
// Args:
// - abciEvent: The event under test
// - key: The key for which we'll check the value
// - want: The desired value
func EventHasAttribueValue(abciEvent sdk.Event, key string, want string) error {
attr, ok := abciEvent.GetAttribute(key)
if !ok {
return fmt.Errorf("abci event does not contain key: %s", key)
}

got := attr.Value
if !strings.Contains(got, want) {
return fmt.Errorf("expected %s %s, got %s", key, want, got)
}

return nil
}
201 changes: 141 additions & 60 deletions x/perp/v2/integration/assertion/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,93 +10,174 @@ import (
"github.com/gogo/protobuf/proto"

"github.com/NibiruChain/nibiru/app"
"github.com/NibiruChain/nibiru/x/common/testutil"
"github.com/NibiruChain/nibiru/x/common/testutil/action"
types "github.com/NibiruChain/nibiru/x/perp/v2/types"
)

type positionChangedEventShouldBeEqual struct {
ExpectedEvent *types.PositionChangedEvent
var _ action.Action = (*containsLiquidateEvent)(nil)
var _ action.Action = (*positionChangedEventShouldBeEqual)(nil)

// TODO test(perp): Add action for testing the appearance of of successful
// liquidation events.

// PositionChangedEventShouldBeEqual checks that the position changed event is
// equal to the expected event.
func PositionChangedEventShouldBeEqual(
expectedEvent *types.PositionChangedEvent,
) action.Action {
return positionChangedEventShouldBeEqual{
ExpectedEvent: expectedEvent,
}
}

func (p positionChangedEventShouldBeEqual) Do(_ *app.NibiruApp, ctx sdk.Context) (sdk.Context, error, bool) {
for _, abciEvent := range ctx.EventManager().Events() {
if abciEvent.Type != proto.MessageName(p.ExpectedEvent) {
continue
}
typedEvent, err := sdk.ParseTypedEvent(abci.Event{
Type: abciEvent.Type,
Attributes: abciEvent.Attributes,
})
if err != nil {
return ctx, err, false
}
// ContainsLiquidateEvent checks if a typed event (proto.Message) is contained in the
// event manager of the app context.
func ContainsLiquidateEvent(
expectedEvent types.LiquidationFailedEvent,
) action.Action {
return containsLiquidateEvent{
ExpectedEvent: expectedEvent,
}
}

theEvent, ok := typedEvent.(*types.PositionChangedEvent)
if !ok {
return ctx, fmt.Errorf("expected event is not of type PositionChangedEvent"), false
}
// eventEquals exports functions for comparing sdk.Events to concrete typed
// events implemented as proto.Message instances in Nibiru.
var eventEquals = iEventEquals{}

if err := types.PositionsAreEqual(&p.ExpectedEvent.FinalPosition, &theEvent.FinalPosition); err != nil {
return ctx, err, false
}
type iEventEquals struct{}

fieldErrs := []string{}
if !theEvent.PositionNotional.Equal(p.ExpectedEvent.PositionNotional) {
err := fmt.Errorf("expected position notional %s, got %s", p.ExpectedEvent.PositionNotional, theEvent.PositionNotional)
fieldErrs = append(fieldErrs, err.Error())
}
// --------------------------------------------------
// --------------------------------------------------

if !theEvent.TransactionFee.Equal(p.ExpectedEvent.TransactionFee) {
err := fmt.Errorf("expected transaction fee %s, got %s", p.ExpectedEvent.TransactionFee, theEvent.TransactionFee)
fieldErrs = append(fieldErrs, err.Error())
}
type containsLiquidateEvent struct {
ExpectedEvent types.LiquidationFailedEvent
}

if !theEvent.RealizedPnl.Equal(p.ExpectedEvent.RealizedPnl) {
err := fmt.Errorf("expected realized pnl %s, got %s", p.ExpectedEvent.RealizedPnl, theEvent.RealizedPnl)
fieldErrs = append(fieldErrs, err.Error())
func (act containsLiquidateEvent) Do(_ *app.NibiruApp, ctx sdk.Context) (
outCtx sdk.Context, err error, isMandatory bool,
) {
wantEvent := act.ExpectedEvent
isEventContained := false
events := ctx.EventManager().Events()
eventsOfMatchingType := []abci.Event{}
for idx, sdkEvent := range events {
err := eventEquals.LiquidationFailedEvent(sdkEvent, wantEvent, idx)
if err == nil {
isEventContained = true
break
} else if sdkEvent.Type != "nibiru.perp.v2.LiquidationFailedEvent" {
continue
} else if sdkEvent.Type == "nibiru.perp.v2.LiquidationFailedEvent" && err != nil {
abciEvent := abci.Event{
Type: sdkEvent.Type,
Attributes: sdkEvent.Attributes,
}
eventsOfMatchingType = append(eventsOfMatchingType, abciEvent)
}
}

if isEventContained {
// happy path
return ctx, nil, true
} else {
// Show descriptive error messages if the expected event is missing
wantEventJson, _ := testutil.ProtoToJson(&wantEvent)
var matchingEvents string = sdk.StringifyEvents(eventsOfMatchingType).String()
return ctx, errors.New(
strings.Join([]string{
fmt.Sprintf("expected the context event manager to contain event: %s.", wantEventJson),
fmt.Sprintf("found %v events:", len(events)),
fmt.Sprintf("events of matching type:\n%v", matchingEvents),
}, "\n"),
), false
}
}

if !theEvent.BadDebt.Equal(p.ExpectedEvent.BadDebt) {
err := fmt.Errorf("expected bad debt %s, got %s", p.ExpectedEvent.BadDebt, theEvent.BadDebt)
func (ee iEventEquals) LiquidationFailedEvent(
sdkEvent sdk.Event, tevent types.LiquidationFailedEvent, eventIdx int,
) error {
fieldErrs := []string{fmt.Sprintf("[DEBUG eventIdx: %v]", eventIdx)}

for _, keyWantPair := range []struct {
key string
want string
}{
{"pair", tevent.Pair.String()},
{"trader", tevent.Trader},
{"liquidator", tevent.Liquidator},
{"reason", tevent.Reason.String()},
} {
if err := testutil.EventHasAttribueValue(sdkEvent, keyWantPair.key, keyWantPair.want); err != nil {
fieldErrs = append(fieldErrs, err.Error())
}
}

if !theEvent.FundingPayment.Equal(p.ExpectedEvent.FundingPayment) {
err := fmt.Errorf("expected funding payment %s, got %s", p.ExpectedEvent.FundingPayment, theEvent.FundingPayment)
if len(fieldErrs) != 1 {
return errors.New(strings.Join(fieldErrs, ". "))
}
return nil
}

func (ee iEventEquals) PositionChangedEvent(
sdkEvent sdk.Event, tevent types.PositionChangedEvent, eventIdx int,
) error {
fieldErrs := []string{fmt.Sprintf("[DEBUG eventIdx: %v]", eventIdx)}

for _, keyWantPair := range []struct {
key string
want string
}{
{"position_notional", tevent.PositionNotional.String()},
{"transaction_fee", tevent.TransactionFee.String()},
{"bad_debt", tevent.BadDebt.String()},
{"realized_pnl", tevent.RealizedPnl.String()},
{"funding_payment", tevent.FundingPayment.String()},
{"block_height", fmt.Sprintf("%v", tevent.BlockHeight)},
{"margin_to_user", tevent.MarginToUser.String()},
{"change_reason", string(tevent.ChangeReason)},
} {
if err := testutil.EventHasAttribueValue(sdkEvent, keyWantPair.key, keyWantPair.want); err != nil {
fieldErrs = append(fieldErrs, err.Error())
}
}

if theEvent.BlockHeight != p.ExpectedEvent.BlockHeight {
err := fmt.Errorf("expected block height %d, got %d", p.ExpectedEvent.BlockHeight, theEvent.BlockHeight)
fieldErrs = append(fieldErrs, err.Error())
if len(fieldErrs) != 1 {
return errors.New(strings.Join(fieldErrs, ". "))
}
return nil
}

type positionChangedEventShouldBeEqual struct {
ExpectedEvent *types.PositionChangedEvent
}

func (p positionChangedEventShouldBeEqual) Do(_ *app.NibiruApp, ctx sdk.Context) (sdk.Context, error, bool) {
for eventIdx, gotSdkEvent := range ctx.EventManager().Events() {
if gotSdkEvent.Type != proto.MessageName(p.ExpectedEvent) {
continue
}
gotProtoMessage, err := sdk.ParseTypedEvent(abci.Event{
Type: gotSdkEvent.Type,
Attributes: gotSdkEvent.Attributes,
})
if err != nil {
return ctx, err, false
}

if !theEvent.MarginToUser.Equal(p.ExpectedEvent.MarginToUser) {
err := fmt.Errorf("expected exchanged margin %s, got %s",
p.ExpectedEvent.MarginToUser, theEvent.MarginToUser)
fieldErrs = append(fieldErrs, err.Error())
gotTypedEvent, ok := gotProtoMessage.(*types.PositionChangedEvent)
if !ok {
return ctx, fmt.Errorf("expected event is not of type PositionChangedEvent"), false
}

if theEvent.ChangeReason != p.ExpectedEvent.ChangeReason {
err := fmt.Errorf("expected change type %s, got %s",
p.ExpectedEvent.ChangeReason, theEvent.ChangeReason)
fieldErrs = append(fieldErrs, err.Error())
if err := types.PositionsAreEqual(&p.ExpectedEvent.FinalPosition, &gotTypedEvent.FinalPosition); err != nil {
return ctx, err, false
}

if len(fieldErrs) != 0 {
err := strings.Join(fieldErrs, "\n")
return ctx, errors.New(err), false
if err := eventEquals.PositionChangedEvent(gotSdkEvent, *gotTypedEvent, eventIdx); err != nil {
return ctx, err, false
}
}

return ctx, nil, false
}

// PositionChangedEventShouldBeEqual checks that the position changed event is equal to the expected event.
func PositionChangedEventShouldBeEqual(
expectedEvent *types.PositionChangedEvent,
) action.Action {
return positionChangedEventShouldBeEqual{
ExpectedEvent: expectedEvent,
}
}
Loading