Skip to content

Commit

Permalink
Protorev: Backrun event emission (#4878)
Browse files Browse the repository at this point in the history
* 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 62968cc)
  • Loading branch information
NotJeremyLiu authored and mergify[bot] committed Apr 19, 2023
1 parent f99cc7b commit dc93fb9
Show file tree
Hide file tree
Showing 10 changed files with 209 additions and 30 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,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

Expand Down
35 changes: 35 additions & 0 deletions x/protorev/keeper/emit.go
Original file line number Diff line number Diff line change
@@ -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)
}
62 changes: 62 additions & 0 deletions x/protorev/keeper/emit_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
7 changes: 4 additions & 3 deletions x/protorev/keeper/posthandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
10 changes: 10 additions & 0 deletions x/protorev/keeper/posthandler_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package keeper_test

import (
"strconv"
"strings"
"testing"

Expand Down Expand Up @@ -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)
}
Expand Down
53 changes: 30 additions & 23 deletions x/protorev/keeper/rebalance.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
}
15 changes: 13 additions & 2 deletions x/protorev/keeper/rebalance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -300,6 +301,7 @@ func (suite *KeeperTestSuite) TestFindMaxProfitRoute() {
suite.Ctx,
route,
&remainingPoolPoints,
&remainingBlockPoolPoints,
)

if test.expectPass {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
})
Expand Down
2 changes: 1 addition & 1 deletion x/protorev/keeper/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
37 changes: 36 additions & 1 deletion x/protorev/protorev.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| 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.
17 changes: 17 additions & 0 deletions x/protorev/types/events.go
Original file line number Diff line number Diff line change
@@ -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"
)

0 comments on commit dc93fb9

Please sign in to comment.