From bd809cf28e36e3e9be3ca74de8b1f7e3f091aa1c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 19 Apr 2023 20:50:56 +0200 Subject: [PATCH] Protorev: Backrun event emission (#4878) (#4963) * Add protorev backrun_event emission * add empty backrun event to pass in as param to make rebalance tests pass * Add basic validity test for protorev_backrun event * Add pool id to event, add documentation * Add tx_hash to backrun event * change pool point var naming * Consolidate create and emit backrun event functions into one * Update logic to not require an additional kvstore read to get data for event - Instead of reading from store to create the event (previously read to determine remaining block-level pool points remaining), use a previously read variable and passes it through the necessary functions to be able to emit it at the end * Rename pool points fn to be more general * Add test to verify proper event emission * add tx_hash to event documentation * Remove unused error return * Update godoc and naming for more clarity * add changelog entry (cherry picked from commit 62968ccb01bfae7958a0952e75b8ee9ac90b2bfd) Co-authored-by: Jeremy Liu <31809888+NotJeremyLiu@users.noreply.github.com> --- CHANGELOG.md | 1 + x/protorev/keeper/emit.go | 35 +++++++++++++++ x/protorev/keeper/emit_test.go | 62 +++++++++++++++++++++++++++ x/protorev/keeper/posthandler.go | 7 +-- x/protorev/keeper/posthandler_test.go | 10 +++++ x/protorev/keeper/rebalance.go | 53 +++++++++++++---------- x/protorev/keeper/rebalance_test.go | 15 ++++++- x/protorev/keeper/routes.go | 2 +- x/protorev/protorev.md | 37 +++++++++++++++- x/protorev/types/events.go | 17 ++++++++ 10 files changed, 209 insertions(+), 30 deletions(-) create mode 100644 x/protorev/keeper/emit.go create mode 100644 x/protorev/keeper/emit_test.go create mode 100644 x/protorev/types/events.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 99e37357d87..094036c59f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features * [#4829] (https://github.com/osmosis-labs/osmosis/pull/4829) Add highest liquidity pool query in x/protorev + * [#4878] (https://github.com/osmosis-labs/osmosis/pull/4878) Emit backrun event upon successful protorev backrun ### Misc Improvements diff --git a/x/protorev/keeper/emit.go b/x/protorev/keeper/emit.go new file mode 100644 index 00000000000..9e84d07f4b0 --- /dev/null +++ b/x/protorev/keeper/emit.go @@ -0,0 +1,35 @@ +package keeper + +import ( + "encoding/hex" + "strconv" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/tendermint/tendermint/crypto/tmhash" + + "github.com/osmosis-labs/osmosis/v15/x/protorev/types" +) + +// EmitBackrunEvent updates and emits a backrunEvent +func EmitBackrunEvent(ctx sdk.Context, pool SwapToBackrun, inputCoin sdk.Coin, profit, tokenOutAmount sdk.Int, remainingTxPoolPoints, remainingBlockPoolPoints uint64) { + // Get tx hash + txHash := strings.ToUpper(hex.EncodeToString(tmhash.Sum(ctx.TxBytes()))) + // Update the backrun event and add it to the context + backrunEvent := sdk.NewEvent( + types.TypeEvtBackrun, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(types.AttributeKeyTxHash, txHash), + sdk.NewAttribute(types.AttributeKeyUserPoolId, strconv.FormatUint(pool.PoolId, 10)), + sdk.NewAttribute(types.AttributeKeyUserDenomIn, pool.TokenInDenom), + sdk.NewAttribute(types.AttributeKeyUserDenomOut, pool.TokenOutDenom), + sdk.NewAttribute(types.AttributeKeyTxPoolPointsRemaining, strconv.FormatUint(remainingTxPoolPoints, 10)), + sdk.NewAttribute(types.AttributeKeyBlockPoolPointsRemaining, strconv.FormatUint(remainingBlockPoolPoints, 10)), + sdk.NewAttribute(types.AttributeKeyProtorevProfit, profit.String()), + sdk.NewAttribute(types.AttributeKeyProtorevAmountIn, inputCoin.Amount.String()), + sdk.NewAttribute(types.AttributeKeyProtorevAmountOut, tokenOutAmount.String()), + sdk.NewAttribute(types.AttributeKeyProtorevArbDenom, inputCoin.Denom), + ) + ctx.EventManager().EmitEvent(backrunEvent) +} diff --git a/x/protorev/keeper/emit_test.go b/x/protorev/keeper/emit_test.go new file mode 100644 index 00000000000..1720fd25a13 --- /dev/null +++ b/x/protorev/keeper/emit_test.go @@ -0,0 +1,62 @@ +package keeper_test + +import ( + "encoding/hex" + "strconv" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/tendermint/tendermint/crypto/tmhash" + + "github.com/osmosis-labs/osmosis/v15/x/protorev/keeper" + "github.com/osmosis-labs/osmosis/v15/x/protorev/types" +) + +func (suite *KeeperTestSuite) TestBackRunEvent() { + testcases := map[string]struct { + pool keeper.SwapToBackrun + remainingTxPoolPoints uint64 + remainingBlockPoolPoints uint64 + profit sdk.Int + tokenOutAmount sdk.Int + inputCoin sdk.Coin + }{ + "basic valid": { + pool: keeper.SwapToBackrun{ + PoolId: 1, + TokenInDenom: "uosmo", + TokenOutDenom: "uatom", + }, + remainingTxPoolPoints: 100, + remainingBlockPoolPoints: 100, + profit: sdk.NewInt(100), + tokenOutAmount: sdk.NewInt(100), + inputCoin: sdk.NewCoin("uosmo", sdk.NewInt(100)), + }, + } + + for name, tc := range testcases { + suite.Run(name, func() { + expectedEvent := sdk.NewEvent( + types.TypeEvtBackrun, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(types.AttributeKeyTxHash, strings.ToUpper(hex.EncodeToString(tmhash.Sum(suite.Ctx.TxBytes())))), + sdk.NewAttribute(types.AttributeKeyUserPoolId, strconv.FormatUint(tc.pool.PoolId, 10)), + sdk.NewAttribute(types.AttributeKeyUserDenomIn, tc.pool.TokenInDenom), + sdk.NewAttribute(types.AttributeKeyUserDenomOut, tc.pool.TokenOutDenom), + sdk.NewAttribute(types.AttributeKeyTxPoolPointsRemaining, strconv.FormatUint(tc.remainingTxPoolPoints, 10)), + sdk.NewAttribute(types.AttributeKeyBlockPoolPointsRemaining, strconv.FormatUint(tc.remainingBlockPoolPoints, 10)), + sdk.NewAttribute(types.AttributeKeyProtorevProfit, tc.profit.String()), + sdk.NewAttribute(types.AttributeKeyProtorevAmountIn, tc.inputCoin.Amount.String()), + sdk.NewAttribute(types.AttributeKeyProtorevAmountOut, tc.tokenOutAmount.String()), + sdk.NewAttribute(types.AttributeKeyProtorevArbDenom, tc.inputCoin.Denom), + ) + + keeper.EmitBackrunEvent(suite.Ctx, tc.pool, tc.inputCoin, tc.profit, tc.tokenOutAmount, tc.remainingTxPoolPoints, tc.remainingBlockPoolPoints) + + // Get last event emitted and ensure it is the expected event + actualEvent := suite.Ctx.EventManager().Events()[len(suite.Ctx.EventManager().Events())-1] + suite.Equal(expectedEvent, actualEvent) + }) + } +} diff --git a/x/protorev/keeper/posthandler.go b/x/protorev/keeper/posthandler.go index 5ba2961e13c..295a36969cc 100644 --- a/x/protorev/keeper/posthandler.go +++ b/x/protorev/keeper/posthandler.go @@ -114,21 +114,22 @@ func (k Keeper) ProtoRevTrade(ctx sdk.Context, swappedPools []SwapToBackrun) (er }() // Get the total number of pool points that can be consumed in this transaction - remainingPoolPoints, err := k.RemainingPoolPointsForTx(ctx) + remainingTxPoolPoints, remainingBlockPoolPoints, err := k.GetRemainingPoolPoints(ctx) if err != nil { return err } + // Iterate and build arbitrage routes for each pool that was swapped on for _, pool := range swappedPools { // Build the routes for the pool that was swapped on routes := k.BuildRoutes(ctx, pool.TokenInDenom, pool.TokenOutDenom, pool.PoolId) // Find optimal route (input coin, profit, route) for the given routes - maxProfitInputCoin, maxProfitAmount, optimalRoute := k.IterateRoutes(ctx, routes, &remainingPoolPoints) + maxProfitInputCoin, maxProfitAmount, optimalRoute := k.IterateRoutes(ctx, routes, &remainingTxPoolPoints, &remainingBlockPoolPoints) // The error that returns here is particularly focused on the minting/burning of coins, and the execution of the MultiHopSwapExactAmountIn. if maxProfitAmount.GT(sdk.ZeroInt()) { - if err := k.ExecuteTrade(ctx, optimalRoute, maxProfitInputCoin); err != nil { + if err := k.ExecuteTrade(ctx, optimalRoute, maxProfitInputCoin, pool, remainingTxPoolPoints, remainingBlockPoolPoints); err != nil { return err } } diff --git a/x/protorev/keeper/posthandler_test.go b/x/protorev/keeper/posthandler_test.go index 1e07cd1bef2..dc654197e1c 100644 --- a/x/protorev/keeper/posthandler_test.go +++ b/x/protorev/keeper/posthandler_test.go @@ -1,6 +1,7 @@ package keeper_test import ( + "strconv" "strings" "testing" @@ -534,6 +535,15 @@ func (suite *KeeperTestSuite) TestAnteHandle() { suite.Require().NoError(err) suite.Require().Equal(tc.params.expectedPoolPoints, pointCount) + _, remainingBlockPoolPoints, err := suite.App.ProtoRevKeeper.GetRemainingPoolPoints(suite.Ctx) + + lastEvent := suite.Ctx.EventManager().Events()[len(suite.Ctx.EventManager().Events())-1] + for _, attr := range lastEvent.Attributes { + if string(attr.Key) == "block_pool_points_remaining" { + suite.Require().Equal(strconv.FormatUint(remainingBlockPoolPoints, 10), string(attr.Value)) + } + } + } else { suite.Require().Error(err) } diff --git a/x/protorev/keeper/rebalance.go b/x/protorev/keeper/rebalance.go index a0cc071b01e..f20a8664b77 100644 --- a/x/protorev/keeper/rebalance.go +++ b/x/protorev/keeper/rebalance.go @@ -9,20 +9,20 @@ import ( // IterateRoutes checks the profitability of every single route that is passed in // and returns the optimal route if there is one -func (k Keeper) IterateRoutes(ctx sdk.Context, routes []RouteMetaData, remainingPoolPoints *uint64) (sdk.Coin, sdk.Int, poolmanagertypes.SwapAmountInRoutes) { +func (k Keeper) IterateRoutes(ctx sdk.Context, routes []RouteMetaData, remainingTxPoolPoints, remainingBlockPoolPoints *uint64) (sdk.Coin, sdk.Int, poolmanagertypes.SwapAmountInRoutes) { var optimalRoute poolmanagertypes.SwapAmountInRoutes var maxProfitInputCoin sdk.Coin maxProfit := sdk.ZeroInt() // Iterate through the routes and find the optimal route for the given swap - for index := 0; index < len(routes) && *remainingPoolPoints > 0; index++ { + for index := 0; index < len(routes) && *remainingTxPoolPoints > 0; index++ { // If the route consumes more pool points than we have remaining then we skip it - if routes[index].PoolPoints > *remainingPoolPoints { + if routes[index].PoolPoints > *remainingTxPoolPoints { continue } // Find the max profit for the route if it exists - inputCoin, profit, err := k.FindMaxProfitForRoute(ctx, routes[index], remainingPoolPoints) + inputCoin, profit, err := k.FindMaxProfitForRoute(ctx, routes[index], remainingTxPoolPoints, remainingBlockPoolPoints) if err != nil { k.Logger(ctx).Error("Error finding max profit for route: ", err) continue @@ -96,7 +96,7 @@ func (k Keeper) EstimateMultihopProfit(ctx sdk.Context, inputDenom string, amoun } // FindMaxProfitRoute runs a binary search to find the max profit for a given route -func (k Keeper) FindMaxProfitForRoute(ctx sdk.Context, route RouteMetaData, remainingPoolPoints *uint64) (sdk.Coin, sdk.Int, error) { +func (k Keeper) FindMaxProfitForRoute(ctx sdk.Context, route RouteMetaData, remainingTxPoolPoints, remainingBlockPoolPoints *uint64) (sdk.Coin, sdk.Int, error) { // Track the tokenIn amount/denom and the profit tokenIn := sdk.Coin{} profit := sdk.ZeroInt() @@ -119,7 +119,9 @@ func (k Keeper) FindMaxProfitForRoute(ctx sdk.Context, route RouteMetaData, rema } // Decrement the number of pool points remaining since we know this route will be profitable - *remainingPoolPoints -= route.PoolPoints + *remainingTxPoolPoints -= route.PoolPoints + *remainingBlockPoolPoints -= route.PoolPoints + // Increment the number of pool points consumed since we know this route will be profitable if err := k.IncrementPointCountForBlock(ctx, route.PoolPoints); err != nil { return sdk.Coin{}, sdk.ZeroInt(), err @@ -187,7 +189,7 @@ func (k Keeper) ExtendSearchRangeIfNeeded(ctx sdk.Context, route RouteMetaData, } // ExecuteTrade inputs a route, amount in, and rebalances the pool -func (k Keeper) ExecuteTrade(ctx sdk.Context, route poolmanagertypes.SwapAmountInRoutes, inputCoin sdk.Coin) error { +func (k Keeper) ExecuteTrade(ctx sdk.Context, route poolmanagertypes.SwapAmountInRoutes, inputCoin sdk.Coin, pool SwapToBackrun, remainingTxPoolPoints, remainingBlockPoolPoints uint64) error { // Get the module address which will execute the trade protorevModuleAddress := k.accountKeeper.GetModuleAddress(types.ModuleName) @@ -220,37 +222,42 @@ func (k Keeper) ExecuteTrade(ctx sdk.Context, route poolmanagertypes.SwapAmountI return err } + // Create and emit the backrun event and add it to the context + EmitBackrunEvent(ctx, pool, inputCoin, profit, tokenOutAmount, remainingTxPoolPoints, remainingBlockPoolPoints) + return nil } -// RemainingPoolPointsForTx calculates the number of pool points that can be consumed in the current transaction. -func (k Keeper) RemainingPoolPointsForTx(ctx sdk.Context) (uint64, error) { - maxRoutesPerTx, err := k.GetMaxPointsPerTx(ctx) +// RemainingPoolPointsForTx calculates the number of pool points that can be consumed in the transaction and block. +// When the remaining pool points for the block is less than the remaining pool points for the transaction, then both +// returned values will be the same, which will be the remaining pool points for the block. +func (k Keeper) GetRemainingPoolPoints(ctx sdk.Context) (uint64, uint64, error) { + maxPoolPointsPerTx, err := k.GetMaxPointsPerTx(ctx) if err != nil { - return 0, err + return 0, 0, err } - maxRoutesPerBlock, err := k.GetMaxPointsPerBlock(ctx) + maxPoolPointsPerBlock, err := k.GetMaxPointsPerBlock(ctx) if err != nil { - return 0, err + return 0, 0, err } - currentRouteCount, err := k.GetPointCountForBlock(ctx) + currentPoolPointsUsedForBlock, err := k.GetPointCountForBlock(ctx) if err != nil { - return 0, err + return 0, 0, err } - // Edge case where the number of routes consumed in the current block is greater than the max number of routes per block + // Edge case where the number of pool points consumed in the current block is greater than the max number of routes per block // This should never happen, but we need to handle it just in case (deal with overflow) - if currentRouteCount >= maxRoutesPerBlock { - return 0, nil + if currentPoolPointsUsedForBlock >= maxPoolPointsPerBlock { + return 0, 0, nil } - // Calculate the number of routes that can be iterated over - numberOfIterableRoutes := maxRoutesPerBlock - currentRouteCount - if numberOfIterableRoutes > maxRoutesPerTx { - return maxRoutesPerTx, nil + // Calculate the number of pool points that can be iterated over + numberOfAvailablePoolPointsForBlock := maxPoolPointsPerBlock - currentPoolPointsUsedForBlock + if numberOfAvailablePoolPointsForBlock > maxPoolPointsPerTx { + return maxPoolPointsPerTx, numberOfAvailablePoolPointsForBlock, nil } - return numberOfIterableRoutes, nil + return numberOfAvailablePoolPointsForBlock, numberOfAvailablePoolPointsForBlock, nil } diff --git a/x/protorev/keeper/rebalance_test.go b/x/protorev/keeper/rebalance_test.go index 0dc6faaf2e6..6aaa42c1b75 100644 --- a/x/protorev/keeper/rebalance_test.go +++ b/x/protorev/keeper/rebalance_test.go @@ -290,6 +290,7 @@ func (suite *KeeperTestSuite) TestFindMaxProfitRoute() { suite.Run(test.name, func() { // init the route remainingPoolPoints := uint64(1000) + remainingBlockPoolPoints := uint64(1000) route := protorevtypes.RouteMetaData{ Route: test.param.route, PoolPoints: test.param.routePoolPoints, @@ -300,6 +301,7 @@ func (suite *KeeperTestSuite) TestFindMaxProfitRoute() { suite.Ctx, route, &remainingPoolPoints, + &remainingBlockPoolPoints, ) if test.expectPass { @@ -388,10 +390,18 @@ func (suite *KeeperTestSuite) TestExecuteTrade() { for _, test := range tests { + // Empty SwapToBackrun var to pass in as param + pool := protorevtypes.SwapToBackrun{} + txPoolPointsRemaining := uint64(100) + blockPoolPointsRemaining := uint64(100) + err := suite.App.ProtoRevKeeper.ExecuteTrade( suite.Ctx, test.param.route, test.param.inputCoin, + pool, + txPoolPointsRemaining, + blockPoolPointsRemaining, ) if test.expectPass { @@ -508,8 +518,9 @@ func (suite *KeeperTestSuite) TestIterateRoutes() { } // Set a high default pool points so that all routes are considered remainingPoolPoints := uint64(40) + remainingBlockPoolPoints := uint64(40) - maxProfitInputCoin, maxProfitAmount, optimalRoute := suite.App.ProtoRevKeeper.IterateRoutes(suite.Ctx, routes, &remainingPoolPoints) + maxProfitInputCoin, maxProfitAmount, optimalRoute := suite.App.ProtoRevKeeper.IterateRoutes(suite.Ctx, routes, &remainingPoolPoints, &remainingBlockPoolPoints) if test.expectPass { suite.Require().Equal(test.params.expectedMaxProfitAmount, maxProfitAmount) suite.Require().Equal(test.params.expectedMaxProfitInputCoin, maxProfitInputCoin) @@ -624,7 +635,7 @@ func (suite *KeeperTestSuite) TestRemainingPoolPointsForTx() { suite.App.ProtoRevKeeper.SetMaxPointsPerBlock(suite.Ctx, tc.maxRoutesPerBlock) suite.App.ProtoRevKeeper.SetPointCountForBlock(suite.Ctx, tc.currentRouteCount) - points, err := suite.App.ProtoRevKeeper.RemainingPoolPointsForTx(suite.Ctx) + points, _, err := suite.App.ProtoRevKeeper.GetRemainingPoolPoints(suite.Ctx) suite.Require().NoError(err) suite.Require().Equal(tc.expectedPointCount, points) }) diff --git a/x/protorev/keeper/routes.go b/x/protorev/keeper/routes.go index 3d722686077..becba437735 100644 --- a/x/protorev/keeper/routes.go +++ b/x/protorev/keeper/routes.go @@ -180,7 +180,7 @@ func (k Keeper) CalculateRoutePoolPoints(ctx sdk.Context, route poolmanagertypes } } - remainingPoolPoints, err := k.RemainingPoolPointsForTx(ctx) + remainingPoolPoints, _, err := k.GetRemainingPoolPoints(ctx) if err != nil { return 0, err } diff --git a/x/protorev/protorev.md b/x/protorev/protorev.md index 86adc0331be..e9ed4d5289d 100644 --- a/x/protorev/protorev.md +++ b/x/protorev/protorev.md @@ -696,4 +696,39 @@ osmosisd query protorev params | POST | /osmosis/v14/protorev/set_max_pool_points_per_tx | Sets the maximum number of pool points that can be consumed per transaction | | POST | /osmosis/v14/protorev/set_max_pool_points_per_block | Sets the maximum number of pool points that can be consumed per block | | POST | /osmosis/v14/protorev/set_pool_weights | Sets the amount of pool points each pool type will consume when executing and simulating trades | -| POST | /osmosis/v14/protorev/set_base_denoms | Sets the base denominations that will be used by ProtoRev to construct cyclic arbitrage routes | \ No newline at end of file +| POST | /osmosis/v14/protorev/set_base_denoms | Sets the base denominations that will be used by ProtoRev to construct cyclic arbitrage routes | + +## Events + +There is 1 type of event that exists in ProtoRev: + +* `types.TypeEvtBackrun` - "protorev_backrun" + +### `types.TypeEvtBackrun` + +This event is emitted after ProtoRev succesfully backruns a transaction. + +It consists of the following attributes: + +* `types.AttributeValueCategory` - "ModuleName" + * The value is the module's name - "protorev". +* `types.AttributeKeyUserPoolId` + * The value is the pool id that the user swapped on that ProtoRev backran. +* `types.AttributeKeyTxHash` + * The value is the transaction hash that ProtoRev backran. +* `types.AttributeKeyUserDenomIn` + * The value is the user denom in for the swap ProtoRev backran. +* `types.AttributeKeyUserDenomOut` + * The value is the user denom out for the swap ProtoRev backran. +* `types.AttributeKeyBlockPoolPointsRemaining` + * The value is the remaining block pool points ProtoRev can still use after the backrun. +* `types.AttributeKeyTxPoolPointsRemaining` + * The value is the remaining tx pool points ProtoRev can still use after the backrun. +* `types.AttributeKeyProtorevProfit` + * The value is the profit ProtoRev captured through the backrun. +* `types.AttributeKeyProtorevAmountIn` + * The value is the amount Protorev swapped in to execute the backrun. +* `types.AttributeKeyProtorevAmountOut` + * The value is the amount Protorev got out of the backrun swap. +* `types.AttributeKeyProtorevArbDenom` + * The value is the denom that ProtoRev swapped in/out to execute the backrun. diff --git a/x/protorev/types/events.go b/x/protorev/types/events.go new file mode 100644 index 00000000000..474fec4704d --- /dev/null +++ b/x/protorev/types/events.go @@ -0,0 +1,17 @@ +package types + +const ( + TypeEvtBackrun = "protorev_backrun" + + AttributeValueCategory = ModuleName + AttributeKeyTxHash = "tx_hash" + AttributeKeyUserPoolId = "user_pool_id" + AttributeKeyUserDenomIn = "user_denom_in" + AttributeKeyUserDenomOut = "user_denom_out" + AttributeKeyBlockPoolPointsRemaining = "block_pool_points_remaining" + AttributeKeyTxPoolPointsRemaining = "tx_pool_points_remaining" + AttributeKeyProtorevProfit = "profit" + AttributeKeyProtorevAmountIn = "amount_in" + AttributeKeyProtorevAmountOut = "amount_out" + AttributeKeyProtorevArbDenom = "arb_denom" +)