Skip to content

Commit

Permalink
constraining protorev by # routes traversed
Browse files Browse the repository at this point in the history
  • Loading branch information
davidterpay committed Dec 29, 2022
1 parent d1932e5 commit 5836130
Show file tree
Hide file tree
Showing 7 changed files with 306 additions and 104 deletions.
15 changes: 10 additions & 5 deletions x/protorev/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,20 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState)
panic(err)
}

// Init module parameters
k.SetParams(ctx, genState.Params)

// Init module state
k.SetParams(ctx, genState.Params)
k.SetProtoRevEnabled(ctx, genState.Params.Enabled)
k.SetDaysSinceModuleGenesis(ctx, 0)
k.SetLatestBlockHeight(ctx, uint64(ctx.BlockHeight()))
k.SetRouteCountForBlock(ctx, 0)

// configure max routes per block (default 100)
if err := k.SetMaxRoutesPerBlock(ctx, 100); err != nil {
panic(err)
}

// Default we only allow 3 pools to be arbitraged against per tx
if err := k.SetMaxPools(ctx, 3); err != nil {
// configure max routes per tx (default 6)
if err := k.SetMaxRoutesPerTx(ctx, 6); err != nil {
panic(err)
}

Expand Down
145 changes: 105 additions & 40 deletions x/protorev/keeper/posthandler.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package keeper

import (
"fmt"

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

gammtypes "github.com/osmosis-labs/osmosis/v13/x/gamm/types"
Expand All @@ -27,75 +29,138 @@ func NewProtoRevDecorator(protoRevDecorator Keeper) ProtoRevDecorator {
func (protoRevDec ProtoRevDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
// Create a cache context to execute the posthandler such that
// 1. If there is an error, then the cache context is discarded
// 2. If there is no error, then the cache context is written to the main context
// 2. If there is no error, then the cache context is written to the main context with no gas consumed
cacheCtx, write := ctx.CacheContext()
cacheCtx = cacheCtx.WithGasMeter(sdk.NewInfiniteGasMeter())

txGasWanted := cacheCtx.GasMeter().Limit()
if txGasWanted == 0 {
// Check if the protorev posthandler can be executed
if err := protoRevDec.ProtoRevKeeper.AnteHandleCheck(cacheCtx); err != nil {
return next(ctx, tx, simulate)
}

// Change the gas meter to be an infinite gas meter which allows the entire posthandler to work without consuming any gas
cacheCtx = cacheCtx.WithGasMeter(sdk.NewInfiniteGasMeter())

// Only execute the protoRev module if it is enabled
if enabled, err := protoRevDec.ProtoRevKeeper.GetProtoRevEnabled(cacheCtx); err != nil || !enabled {
// Extract all of the pools that were swapped in the tx
swappedPools := ExtractSwappedPools(tx)
if len(swappedPools) == 0 {
return next(ctx, tx, simulate)
}

// Get the max number of pools to iterate through
maxPoolsToIterate, err := protoRevDec.ProtoRevKeeper.GetMaxPools(cacheCtx)
// Attempt to execute arbitrage trades
if err := protoRevDec.ProtoRevKeeper.ProtoRevTrade(cacheCtx, swappedPools); err == nil {
write()
ctx.EventManager().EmitEvents(cacheCtx.EventManager().Events())
} else {
ctx.Logger().Error("ProtoRevTrade failed with error", err)
}

return next(ctx, tx, simulate)
}

// AnteHandleCheck checks if the module is enabled and if the number of routes to be processed per block has been reached.
func (k Keeper) AnteHandleCheck(ctx sdk.Context) error {
// Only execute the posthandler if the module is enabled
if enabled, err := k.GetProtoRevEnabled(ctx); err != nil || !enabled {
return fmt.Errorf("protorev is not enabled")
}

latestBlockHeight, err := k.GetLatestBlockHeight(ctx)
if err != nil {
return next(ctx, tx, simulate)
return fmt.Errorf("failed to get latest block height")
}

// Find routes for every single pool that was swapped on (up to maxPoolsToIterate pools per tx)
swappedPools := ExtractSwappedPools(tx)
if len(swappedPools) > int(maxPoolsToIterate) {
swappedPools = swappedPools[:int(maxPoolsToIterate)]
currentRouteCount, err := k.GetRouteCountForBlock(ctx)
if err != nil {
return fmt.Errorf("failed to get current route count")
}

tradeErr := error(nil)
for _, swap := range swappedPools {
// If there was is an error executing the trade, break and set tradeErr
if err := protoRevDec.ProtoRevKeeper.ProtoRevTrade(cacheCtx, swap); err != nil {
tradeErr = err
break
}
maxRouteCount, err := k.GetMaxRoutesPerBlock(ctx)
if err != nil {
return fmt.Errorf("failed to get max iterable routes per block")
}

// If there was no error, write the cache context to the main context
if tradeErr == nil {
write()
ctx.EventManager().EmitEvents(cacheCtx.EventManager().Events())
// Only execute the posthandler if the number of routes to be processed per block has not been reached
blockHeight := uint64(ctx.BlockHeight())
if blockHeight == latestBlockHeight {
if currentRouteCount >= maxRouteCount {
return fmt.Errorf("max route count for block has been reached")
}
} else {
ctx.Logger().Error("ProtoRevTrade failed with error", tradeErr)
// Reset the current route count
k.SetRouteCountForBlock(ctx, 0)
k.SetLatestBlockHeight(ctx, blockHeight)
}

return next(ctx, tx, simulate)
return nil
}

// ProtoRevTrade wraps around the build routes, iterate routes, and execute trade functionality to execute an cyclic arbitrage trade
// if it exists. It returns an error if there was an error executing the trade and a boolean if the trade was executed.
func (k Keeper) ProtoRevTrade(ctx sdk.Context, swap SwapToBackrun) error {
// Build the routes for the swap
routes := k.BuildRoutes(ctx, swap.TokenInDenom, swap.TokenOutDenom, swap.PoolId)
// ProtoRevTrade wraps around the build routes, iterate routes, and execute trade functionality to execute cyclic arbitrage trades
// if they exist. It returns an error if there was an issue executing any single trade.
func (k Keeper) ProtoRevTrade(ctx sdk.Context, swappedPools []SwapToBackrun) error {
// Get the total number of routes that can be explored
numberOfIterableRoutes, err := k.CalcNumberOfIterableRoutes(ctx)
if err != nil {
return err
}

// Iterate and build arbitrage routes for each pool that was swapped on
for index := 0; index < len(swappedPools) && numberOfIterableRoutes > 0; index++ {
// Build the routes for the pool that was swapped on
routes := k.BuildRoutes(ctx, swappedPools[index].TokenInDenom, swappedPools[index].TokenOutDenom, swappedPools[index].PoolId)
numRoutes := uint64(len(routes))

if numRoutes != 0 {
// filter out routes that are not iterable
if numberOfIterableRoutes < numRoutes {
routes = routes[:numberOfIterableRoutes]
numRoutes = numberOfIterableRoutes
}

if len(routes) != 0 {
// Find optimal input amounts for routes
maxProfitInputCoin, maxProfitAmount, optimalRoute := k.IterateRoutes(ctx, routes)
// Find optimal input amounts for routes
maxProfitInputCoin, maxProfitAmount, optimalRoute := k.IterateRoutes(ctx, routes)

// 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 {
// Update route counts
if err := k.IncrementRouteCountForBlock(ctx, numRoutes); err != nil {
return err
}
return nil
numberOfIterableRoutes -= numRoutes

// 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 {
return err
}
}
}
}

return nil
}

// CalcNumberOfIterableRoutes calculates the number of routes that can be iterated over in the current transaction
func (k Keeper) CalcNumberOfIterableRoutes(ctx sdk.Context) (uint64, error) {
maxRoutesPerTx, err := k.GetMaxRoutesPerTx(ctx)
if err != nil {
return 0, err
}

maxRoutesPerBlock, err := k.GetMaxRoutesPerBlock(ctx)
if err != nil {
return 0, err
}

currentRouteCount, err := k.GetRouteCountForBlock(ctx)
if err != nil {
return 0, err
}

// Calculate the number of routes that can be iterated over
numberOfIterableRoutes := maxRoutesPerBlock - currentRouteCount
if numberOfIterableRoutes > maxRoutesPerTx {
numberOfIterableRoutes = maxRoutesPerTx
}

return numberOfIterableRoutes, nil
}

// ExtractSwappedPools checks if there were any swaps made on pools and if so returns a list of all the pools that were
// swapped on and metadata about the swap
func ExtractSwappedPools(tx sdk.Tx) []SwapToBackrun {
Expand Down
40 changes: 12 additions & 28 deletions x/protorev/keeper/posthandler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func (suite *KeeperTestSuite) TestAnteHandle() {
baseDenomGas bool
expectedNumOfTrades sdk.Int
expectedProfits []*sdk.Coin
expectedRouteCount uint64
}

txBuilder := suite.clientCtx.TxConfig.NewTxBuilder()
Expand All @@ -50,32 +51,7 @@ func (suite *KeeperTestSuite) TestAnteHandle() {
baseDenomGas: true,
expectedNumOfTrades: sdk.ZeroInt(),
expectedProfits: []*sdk.Coin{},
},
expectPass: true,
},
{
name: "0 Gas Limit - Expect Nothing To Happen",
params: param{
msgs: []sdk.Msg{
&gammtypes.MsgSwapExactAmountIn{
Sender: addr0.String(),
Routes: []gammtypes.SwapAmountInRoute{
{
PoolId: 23,
TokenOutDenom: "ibc/BE1BB42D4BE3C30D50B68D7C41DB4DFCE9678E8EF8C539F6E6A9345048894FCC",
},
},
TokenIn: sdk.NewCoin("ibc/0EF15DF2F02480ADE0BB6E85D9EBB5DAEA2836D3860E9F97F9AADE4F57A31AA0", sdk.NewInt(10000)),
TokenOutMinAmount: sdk.NewInt(10000),
},
},
txFee: sdk.NewCoins(sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(10000))),
minGasPrices: sdk.NewDecCoins(),
gasLimit: 0,
isCheckTx: false,
baseDenomGas: true,
expectedNumOfTrades: sdk.ZeroInt(),
expectedProfits: []*sdk.Coin{},
expectedRouteCount: 0,
},
expectPass: true,
},
Expand All @@ -102,6 +78,7 @@ func (suite *KeeperTestSuite) TestAnteHandle() {
baseDenomGas: true,
expectedNumOfTrades: sdk.ZeroInt(),
expectedProfits: []*sdk.Coin{},
expectedRouteCount: 2,
},
expectPass: true,
},
Expand Down Expand Up @@ -133,6 +110,7 @@ func (suite *KeeperTestSuite) TestAnteHandle() {
Amount: sdk.NewInt(24848),
},
},
expectedRouteCount: 3,
},
expectPass: true,
},
Expand Down Expand Up @@ -168,6 +146,7 @@ func (suite *KeeperTestSuite) TestAnteHandle() {
Amount: sdk.NewInt(24848),
},
},
expectedRouteCount: 4,
},
expectPass: true,
},
Expand All @@ -187,7 +166,7 @@ func (suite *KeeperTestSuite) TestAnteHandle() {
TokenOutMinAmount: sdk.NewInt(100),
},
},
txFee: sdk.NewCoins(sdk.NewCoin("uosmo", sdk.NewInt(10000))),
txFee: sdk.NewCoins(sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(10000))),
minGasPrices: sdk.NewDecCoins(),
gasLimit: 500000,
isCheckTx: false,
Expand All @@ -203,14 +182,14 @@ func (suite *KeeperTestSuite) TestAnteHandle() {
Amount: sdk.NewInt(56609900),
},
},
expectedRouteCount: 5,
},
expectPass: true,
},
}

for _, tc := range tests {
suite.Run(tc.name, func() {

suite.Ctx = suite.Ctx.WithIsCheckTx(tc.params.isCheckTx)
suite.Ctx = suite.Ctx.WithGasMeter(sdk.NewInfiniteGasMeter())
suite.Ctx = suite.Ctx.WithMinGasPrices(tc.params.minGasPrices)
Expand Down Expand Up @@ -265,6 +244,11 @@ func (suite *KeeperTestSuite) TestAnteHandle() {
profits := suite.App.AppKeepers.ProtoRevKeeper.GetAllProfits(suite.Ctx)
suite.Require().Equal(tc.params.expectedProfits, profits)

// Check the current route count
routeCount, err := suite.App.AppKeepers.ProtoRevKeeper.GetRouteCountForBlock(suite.Ctx)
suite.Require().NoError(err)
suite.Require().Equal(tc.params.expectedRouteCount, routeCount)

} else {
suite.Require().Error(err)
}
Expand Down
Loading

0 comments on commit 5836130

Please sign in to comment.