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

Protorev: Backrun event emission #4878

Merged
merged 14 commits into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [#4783](https://github.com/osmosis-labs/osmosis/pull/4783) Update wasmd to 0.31.0
* [#4886](https://github.com/osmosis-labs/osmosis/pull/4886) Implement MsgSplitRouteSwapExactAmountIn and MsgSplitRouteSwapExactAmountOut that supports route splitting.
* [#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 @@ -179,7 +179,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 @@ -702,4 +702,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"
)