From 366b1f4a320e500486ad2171fe1c44ac088c39c8 Mon Sep 17 00:00:00 2001 From: Jeremy Liu <31809888+NotJeremyLiu@users.noreply.github.com> Date: Thu, 16 Feb 2023 13:21:53 -0500 Subject: [PATCH] Protorev smarter logic and more testing (#4181) * Add generalized tests Added: - 4 pool mainnet route test - 2 pool mainnet route test - Non-osmo/atom denom test * Add stableswap doomsday test - Currently panics due to solveCFMMBinarySearchMulti in stableswap calculation * Add tests for pool point limits - Tests that the per tx limit works properly - Tests that the per block limit works properly * Add pre-check before binary search - Adds a pre-check before the binary search that tells us if we need to run the binary search at all - Reduces overall computation/time by avoiding binary searching profit amounts over routes without any profit opportunity * Add denom to make test pass - Added "test/3" denom to test non-osmo/atom denoms in previous PR - didn't notice it broke one test that expected us to only have 2 denoms, so adding "test/3" here so everything passes * Implement minimum change to have smarter binary search bounds - This implements a way for us to have a binary search bound that can increase in size to account for large trades above the default range - I believe this is the minimum-amount-of-new-code approach, but may not be the optimal approach (what this method avoids is having to save the amount in or amount out of the original swap, and then backtracking/converting that amount to the base denom for an arb, and creating bounds off of that) * Switch range increasing logic - Doubling isn't the wanted behavior (only wanted if lower bound is 1), just increasing bound by the same range size is wanted (so adding MaxInputAmount achieves this) * Add logic to extend search bounds when finding optimal amount in - Increases max iterations to 17 to allow for situation when we need to increase the upper bound - Add new ExtendedMaxInputAmount variable for us to use as this new max bound range - Replace bound changing logic from iteratively changing the range to immediately giving our max range * Move range extension into it's own helper function * basic benchmark testing for posthandler and epoch hook * Add SwapAmountOut Test * Add extended range test - Tests binary search range extension logic works properly * Add panic catching test - This currently fails, this is on purpose so we don't merge the PR and think we are good until this passes and the panics are handled properly * dynamic step size * pool points only incremented if profitable * adding sanity checks for pool point calcs, nits * Update doomsday testing accounting for refund system - Makes pool 41 reserves to have an arb opportunity in the doomsday routes - Changes tx limit and block limit specifically for doomsday testing * Return nil for no profit opportunity * adding E2E, removing atom as a base denom, more testing for post handler * find max profit test fix * nit * backporting version tag to 14.x * comment update for protorev admin account --------- Co-authored-by: David Terpay (cherry picked from commit 23b13a1d19fd6f7fb5795676cea89df88796070f) # Conflicts: # tests/e2e/configurer/chain/queries.go --- proto/osmosis/protorev/v1beta1/genesis.proto | 2 - proto/osmosis/protorev/v1beta1/protorev.proto | 6 + tests/e2e/configurer/chain/queries.go | 183 +++++ tests/e2e/containers/config.go | 2 +- tests/e2e/e2e_test.go | 128 ++++ tests/e2e/initialization/config.go | 18 +- tests/e2e/scripts/protorevPool1.json | 7 + tests/e2e/scripts/protorevPool2.json | 7 + tests/e2e/scripts/protorevPool3.json | 7 + x/protorev/genesis.go | 30 +- x/protorev/keeper/developer_fees_test.go | 8 +- x/protorev/keeper/epoch_hook_test.go | 19 + x/protorev/keeper/grpc_query_test.go | 42 +- x/protorev/keeper/keeper_test.go | 246 ++++++- x/protorev/keeper/msg_server.go | 2 +- x/protorev/keeper/msg_server_test.go | 87 ++- x/protorev/keeper/posthandler.go | 71 +- x/protorev/keeper/posthandler_test.go | 628 +++++++++++++++++- x/protorev/keeper/protorev.go | 4 +- x/protorev/keeper/protorev_test.go | 33 +- x/protorev/keeper/rebalance.go | 145 +++- x/protorev/keeper/rebalance_test.go | 357 ++++++++-- x/protorev/keeper/routes.go | 149 ++--- x/protorev/keeper/routes_test.go | 469 ++++++------- x/protorev/keeper/statistics_test.go | 12 +- x/protorev/types/constants.go | 12 +- x/protorev/types/genesis.go | 38 +- x/protorev/types/genesis.pb.go | 86 +-- x/protorev/types/genesis_test.go | 161 ----- x/protorev/types/msg_test.go | 109 ++- x/protorev/types/protorev.pb.go | 133 +++- x/protorev/types/token_pair_arb_routes.go | 6 + 32 files changed, 2308 insertions(+), 899 deletions(-) create mode 100644 tests/e2e/scripts/protorevPool1.json create mode 100644 tests/e2e/scripts/protorevPool2.json create mode 100644 tests/e2e/scripts/protorevPool3.json diff --git a/proto/osmosis/protorev/v1beta1/genesis.proto b/proto/osmosis/protorev/v1beta1/genesis.proto index 7d1defc4290..e3b1ed64020 100644 --- a/proto/osmosis/protorev/v1beta1/genesis.proto +++ b/proto/osmosis/protorev/v1beta1/genesis.proto @@ -11,6 +11,4 @@ option go_package = "github.com/osmosis-labs/osmosis/v14/x/protorev/types"; message GenesisState { // Module Parameters Params params = 1 [ (gogoproto.nullable) = false ]; - // Hot routes that are configured on genesis - repeated TokenPairArbRoutes token_pairs = 2 [ (gogoproto.nullable) = false ]; } \ No newline at end of file diff --git a/proto/osmosis/protorev/v1beta1/protorev.proto b/proto/osmosis/protorev/v1beta1/protorev.proto index 41e66c1cebd..e489e9644e8 100644 --- a/proto/osmosis/protorev/v1beta1/protorev.proto +++ b/proto/osmosis/protorev/v1beta1/protorev.proto @@ -18,6 +18,12 @@ message TokenPairArbRoutes { string token_in = 2; // Token denomination of the second asset string token_out = 3; + // The step size that will be used to find the optimal swap amount in the + // binary search + string step_size = 4 [ + (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int", + (gogoproto.nullable) = true + ]; } // Route is a hot route for a given pair of tokens diff --git a/tests/e2e/configurer/chain/queries.go b/tests/e2e/configurer/chain/queries.go index 8c916e8784e..4be11261fc1 100644 --- a/tests/e2e/configurer/chain/queries.go +++ b/tests/e2e/configurer/chain/queries.go @@ -22,10 +22,181 @@ import ( "github.com/osmosis-labs/osmosis/v14/tests/e2e/util" epochstypes "github.com/osmosis-labs/osmosis/v14/x/epochs/types" gammtypes "github.com/osmosis-labs/osmosis/v14/x/gamm/types" +<<<<<<< HEAD +======= + poolmanagertypes "github.com/osmosis-labs/osmosis/v14/x/poolmanager/types" + protorevtypes "github.com/osmosis-labs/osmosis/v14/x/protorev/types" +>>>>>>> 23b13a1d (Protorev smarter logic and more testing (#4181)) superfluidtypes "github.com/osmosis-labs/osmosis/v14/x/superfluid/types" twapqueryproto "github.com/osmosis-labs/osmosis/v14/x/twap/client/queryproto" ) +// QueryProtoRevNumberOfTrades gets the number of trades the protorev module has executed. +func (n *NodeConfig) QueryProtoRevNumberOfTrades() (sdk.Int, error) { + path := "/osmosis/v14/protorev/number_of_trades" + + bz, err := n.QueryGRPCGateway(path) + if err != nil { + return sdk.Int{}, err + } + + // nolint: staticcheck + var response protorevtypes.QueryGetProtoRevNumberOfTradesResponse + err = util.Cdc.UnmarshalJSON(bz, &response) + require.NoError(n.t, err) // this error should not happen + return response.NumberOfTrades, nil +} + +// QueryProtoRevProfits gets the profits the protorev module has made. +func (n *NodeConfig) QueryProtoRevProfits() ([]*sdk.Coin, error) { + path := "/osmosis/v14/protorev/all_profits" + + bz, err := n.QueryGRPCGateway(path) + if err != nil { + return []*sdk.Coin{}, err + } + + // nolint: staticcheck + var response protorevtypes.QueryGetProtoRevAllProfitsResponse + err = util.Cdc.UnmarshalJSON(bz, &response) + require.NoError(n.t, err) // this error should not happen + return response.Profits, nil +} + +// QueryProtoRevAllRouteStatistics gets all of the route statistics that the module has recorded. +func (n *NodeConfig) QueryProtoRevAllRouteStatistics() ([]protorevtypes.RouteStatistics, error) { + path := "/osmosis/v14/protorev/all_route_statistics" + + bz, err := n.QueryGRPCGateway(path) + if err != nil { + return []protorevtypes.RouteStatistics{}, err + } + + // nolint: staticcheck + var response protorevtypes.QueryGetProtoRevAllRouteStatisticsResponse + err = util.Cdc.UnmarshalJSON(bz, &response) + require.NoError(n.t, err) // this error should not happen + return response.Statistics, nil +} + +// QueryProtoRevTokenPairArbRoutes gets all of the token pair hot routes that the module is currently using. +func (n *NodeConfig) QueryProtoRevTokenPairArbRoutes() ([]*protorevtypes.TokenPairArbRoutes, error) { + path := "/osmosis/v14/protorev/token_pair_arb_routes" + + bz, err := n.QueryGRPCGateway(path) + if err != nil { + return []*protorevtypes.TokenPairArbRoutes{}, err + } + + // nolint: staticcheck + var response protorevtypes.QueryGetProtoRevTokenPairArbRoutesResponse + err = util.Cdc.UnmarshalJSON(bz, &response) + require.NoError(n.t, err) // this error should not happen + return response.Routes, nil +} + +// QueryProtoRevDeveloperAccount gets the developer account of the module. +func (n *NodeConfig) QueryProtoRevDeveloperAccount() (sdk.AccAddress, error) { + path := "/osmosis/v14/protorev/developer_account" + + bz, err := n.QueryGRPCGateway(path) + if err != nil { + return nil, err + } + + // nolint: staticcheck + var response protorevtypes.QueryGetProtoRevDeveloperAccountResponse + err = util.Cdc.UnmarshalJSON(bz, &response) + require.NoError(n.t, err) // this error should not happen + + account, err := sdk.AccAddressFromBech32(response.DeveloperAccount) + if err != nil { + return nil, err + } + + return account, nil +} + +// QueryProtoRevPoolWeights gets the pool point weights of the module. +func (n *NodeConfig) QueryProtoRevPoolWeights() (*protorevtypes.PoolWeights, error) { + path := "/osmosis/v14/protorev/pool_weights" + + bz, err := n.QueryGRPCGateway(path) + if err != nil { + return nil, err + } + + // nolint: staticcheck + var response protorevtypes.QueryGetProtoRevPoolWeightsResponse + err = util.Cdc.UnmarshalJSON(bz, &response) + require.NoError(n.t, err) // this error should not happen + return response.PoolWeights, nil +} + +// QueryProtoRevMaxPoolPointsPerTx gets the max pool points per tx of the module. +func (n *NodeConfig) QueryProtoRevMaxPoolPointsPerTx() (uint64, error) { + path := "/osmosis/v14/protorev/max_pool_points_per_tx" + + bz, err := n.QueryGRPCGateway(path) + if err != nil { + return 0, err + } + + // nolint: staticcheck + var response protorevtypes.QueryGetProtoRevMaxPoolPointsPerTxResponse + err = util.Cdc.UnmarshalJSON(bz, &response) + require.NoError(n.t, err) // this error should not happen + return response.MaxPoolPointsPerTx, nil +} + +// QueryProtoRevMaxPoolPointsPerBlock gets the max pool points per block of the module. +func (n *NodeConfig) QueryProtoRevMaxPoolPointsPerBlock() (uint64, error) { + path := "/osmosis/v14/protorev/max_pool_points_per_block" + + bz, err := n.QueryGRPCGateway(path) + if err != nil { + return 0, err + } + + // nolint: staticcheck + var response protorevtypes.QueryGetProtoRevMaxPoolPointsPerBlockResponse + err = util.Cdc.UnmarshalJSON(bz, &response) + require.NoError(n.t, err) // this error should not happen + return response.MaxPoolPointsPerBlock, nil +} + +// QueryProtoRevBaseDenoms gets the base denoms used to construct cyclic arbitrage routes. +func (n *NodeConfig) QueryProtoRevBaseDenoms() ([]*protorevtypes.BaseDenom, error) { + path := "/osmosis/v14/protorev/base_denoms" + + bz, err := n.QueryGRPCGateway(path) + if err != nil { + return []*protorevtypes.BaseDenom{}, err + } + + // nolint: staticcheck + var response protorevtypes.QueryGetProtoRevBaseDenomsResponse + err = util.Cdc.UnmarshalJSON(bz, &response) + require.NoError(n.t, err) // this error should not happen + return response.BaseDenoms, nil +} + +// QueryProtoRevEnabled queries if the protorev module is enabled. +func (n *NodeConfig) QueryProtoRevEnabled() (bool, error) { + path := "/osmosis/v14/protorev/enabled" + + bz, err := n.QueryGRPCGateway(path) + if err != nil { + return false, err + } + + // nolint: staticcheck + var response protorevtypes.QueryGetProtoRevEnabledResponse + err = util.Cdc.UnmarshalJSON(bz, &response) + require.NoError(n.t, err) // this error should not happen + return response.Enabled, nil +} + func (n *NodeConfig) QueryGRPCGateway(path string, parameters ...string) ([]byte, error) { if len(parameters)%2 != 0 { return nil, fmt.Errorf("invalid number of parameters, must follow the format of key + value") @@ -111,6 +282,18 @@ func (n *NodeConfig) QuerySupplyOf(denom string) (sdk.Int, error) { return supplyResp.Amount.Amount, nil } +func (n *NodeConfig) QuerySupply() (sdk.Coins, error) { + path := "cosmos/bank/v1beta1/supply" + bz, err := n.QueryGRPCGateway(path) + require.NoError(n.t, err) + + var supplyResp banktypes.QueryTotalSupplyResponse + if err := util.Cdc.UnmarshalJSON(bz, &supplyResp); err != nil { + return nil, err + } + return supplyResp.Supply, nil +} + func (n *NodeConfig) QueryContractsFromId(codeId int) ([]string, error) { path := fmt.Sprintf("/cosmwasm/wasm/v1/code/%d/contracts", codeId) bz, err := n.QueryGRPCGateway(path) diff --git a/tests/e2e/containers/config.go b/tests/e2e/containers/config.go index 9ba26d2a61f..fb40fedda62 100644 --- a/tests/e2e/containers/config.go +++ b/tests/e2e/containers/config.go @@ -27,7 +27,7 @@ const ( previousVersionOsmoTag = "v14.x-4d4583fa-1676370337" // Pre-upgrade repo/tag for osmosis initialization (this should be one version below upgradeVersion) previousVersionInitRepository = "osmolabs/osmosis-e2e-init-chain" - previousVersionInitTag = "v14.x-4d4583fa-1676370337-manual" + previousVersionInitTag = "v14.x-937d601e-1676550460-manual" // Hermes repo/version for relayer relayerRepository = "osmolabs/hermes" relayerTag = "0.13.0" diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index d2921804ac4..3fc3848a719 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -34,6 +34,134 @@ import ( // Reusable Checks +// TestProtoRev is a test that ensures that the protorev module is working as expected. In particular, this tests and ensures that: +// 1. The protorev module is correctly configured on init +// 2. The protorev module can correctly back run a swap +// 3. the protorev module correctly tracks statistics +func (s *IntegrationTestSuite) TestProtoRev() { + const ( + poolFile1 = "protorevPool1.json" + poolFile2 = "protorevPool2.json" + poolFile3 = "protorevPool3.json" + + walletName = "swap-that-creates-an-arb" + + denomIn = initialization.LuncIBCDenom + denomOut = initialization.UstIBCDenom + amount = "10000" + minAmountOut = "1" + + epochIdentifier = "week" + ) + + chainA := s.configurer.GetChainConfig(0) + chainANode, err := chainA.GetDefaultNode() + s.NoError(err) + + // --------------- Module init checks ---------------- // + // The module should be enabled by default. + enabled, err := chainANode.QueryProtoRevEnabled() + s.T().Logf("checking that the protorev module is enabled: %t", enabled) + s.Require().NoError(err) + s.Require().True(enabled) + + // The module should have no new hot routes by default. + hotRoutes, err := chainANode.QueryProtoRevTokenPairArbRoutes() + s.T().Logf("checking that the protorev module has no new hot routes: %v", hotRoutes) + s.Require().NoError(err) + s.Require().Len(hotRoutes, 0) + + // The module should have no trades by default. + numTrades, err := chainANode.QueryProtoRevNumberOfTrades() + s.T().Logf("checking that the protorev module has no trades on init: %s", err) + s.Require().Error(err) + + // The module should have pool weights by default. + poolWeights, err := chainANode.QueryProtoRevPoolWeights() + s.T().Logf("checking that the protorev module has pool weights on init: %s", poolWeights) + s.Require().NoError(err) + s.Require().NotNil(poolWeights) + + // The module should have max pool points per tx by default. + maxPoolPointsPerTx, err := chainANode.QueryProtoRevMaxPoolPointsPerTx() + s.T().Logf("checking that the protorev module has max pool points per tx on init: %d", maxPoolPointsPerTx) + s.Require().NoError(err) + + // The module should have max pool points per block by default. + maxPoolPointsPerBlock, err := chainANode.QueryProtoRevMaxPoolPointsPerBlock() + s.T().Logf("checking that the protorev module has max pool points per block on init: %d", maxPoolPointsPerBlock) + s.Require().NoError(err) + + // The module should have only osmosis as a supported base denom by default. + supportedBaseDenoms, err := chainANode.QueryProtoRevBaseDenoms() + s.T().Logf("checking that the protorev module has only osmosis as a supported base denom on init: %v", supportedBaseDenoms) + s.Require().NoError(err) + s.Require().Len(supportedBaseDenoms, 1) + s.Require().Equal(supportedBaseDenoms[0].Denom, "uosmo") + + // The module should have no developer account by default. + _, err = chainANode.QueryProtoRevDeveloperAccount() + s.T().Logf("checking that the protorev module has no developer account on init: %s", err) + s.Require().Error(err) + + // --------------- Set up for a calculated backrun ---------------- // + // Create all of the pools that will be used in the test. + chainANode.CreateBalancerPool(poolFile1, initialization.ValidatorWalletName) + swapPoolId := chainANode.CreateBalancerPool(poolFile2, initialization.ValidatorWalletName) + chainANode.CreateBalancerPool(poolFile3, initialization.ValidatorWalletName) + + // Wait for the creation to be propogated to the other nodes + for the protorev module to + // correctly update the highest liquidity pools. + s.T().Logf("waiting for the protorev module to update the highest liquidity pools (wait %.f sec) after the week epoch duration", initialization.EpochWeekDuration.Seconds()) + chainA.WaitForNumEpochs(1, epochIdentifier) + + // Create a wallet to use for the swap txs. + swapWalletAddr := chainANode.CreateWallet(walletName) + coinIn := fmt.Sprintf("%s%s", amount, denomIn) + chainANode.BankSend(coinIn, chainA.NodeConfigs[0].PublicAddress, swapWalletAddr) + + // Check supplies before swap. + supplyBefore, err := chainANode.QuerySupply() + s.Require().NoError(err) + s.Require().NotNil(supplyBefore) + + // Performing the swap that creates a cyclic arbitrage opportunity. + s.T().Logf("performing a swap that creates a cyclic arbitrage opportunity") + chainANode.SwapExactAmountIn(coinIn, minAmountOut, fmt.Sprintf("%d", swapPoolId), denomOut, swapWalletAddr) + + // --------------- Module checks after a calculated backrun ---------------- // + // Check that the supplies have not changed. + s.T().Logf("checking that the supplies have not changed") + supplyAfter, err := chainANode.QuerySupply() + s.Require().NoError(err) + s.Require().NotNil(supplyAfter) + s.Require().Equal(supplyBefore, supplyAfter) + + // Check that the number of trades executed by the protorev module is 1. + numTrades, err = chainANode.QueryProtoRevNumberOfTrades() + s.T().Logf("checking that the protorev module has executed 1 trade") + s.Require().NoError(err) + s.Require().NotNil(numTrades) + s.Require().Equal(uint64(1), numTrades.Uint64()) + + // Check that the profits of the protorev module are not nil. + profits, err := chainANode.QueryProtoRevProfits() + s.T().Logf("checking that the protorev module has non-nil profits: %s", profits) + s.Require().NoError(err) + s.Require().NotNil(profits) + s.Require().Len(profits, 1) + + // Check that the route statistics of the protorev module are not nil. + routeStats, err := chainANode.QueryProtoRevAllRouteStatistics() + s.T().Logf("checking that the protorev module has non-nil route statistics: %x", routeStats) + s.Require().NoError(err) + s.Require().NotNil(routeStats) + s.Require().Len(routeStats, 1) + s.Require().Equal(sdk.OneInt(), routeStats[0].NumberOfTrades) + s.Require().Equal([]uint64{swapPoolId - 1, swapPoolId, swapPoolId + 1}, routeStats[0].Route) + s.Require().Equal(profits, routeStats[0].Profits) +} + // CheckBalance Checks the balance of an address func (s *IntegrationTestSuite) CheckBalance(node *chain.NodeConfig, addr, denom string, amount int64) { // check the balance of the contract diff --git a/tests/e2e/initialization/config.go b/tests/e2e/initialization/config.go index 71e0da75ebe..d7fec0758d2 100644 --- a/tests/e2e/initialization/config.go +++ b/tests/e2e/initialization/config.go @@ -56,15 +56,19 @@ const ( OsmoIBCDenom = "ibc/ED07A3391A112B175915CD8FAF43A2DA8E4790EDE12566649D0C2F97716B8518" StakeIBCDenom = "ibc/C053D637CCA2A2BA030E2C5EE1B28A16F71CCB0E45E8BE52766DC1B241B7787" E2EFeeToken = "e2e-default-feetoken" + UstIBCDenom = "ibc/BE1BB42D4BE3C30D50B68D7C41DB4DFCE9678E8EF8C539F6E6A9345048894FCC" + LuncIBCDenom = "ibc/0EF15DF2F02480ADE0BB6E85D9EBB5DAEA2836D3860E9F97F9AADE4F57A31AA0" MinGasPrice = "0.000" IbcSendAmount = 3300000000 ValidatorWalletName = "val" // chainA ChainAID = "osmo-test-a" - OsmoBalanceA = 200000000000 + OsmoBalanceA = 20000000000000 IonBalanceA = 100000000000 StakeBalanceA = 110000000000 StakeAmountA = 100000000000 + UstBalanceA = 500000000000000 + LuncBalanceA = 500000000000000 // chainB ChainBID = "osmo-test-b" OsmoBalanceB = 500000000000 @@ -74,8 +78,9 @@ const ( GenesisFeeBalance = 100000000000 WalletFeeBalance = 100000000 - EpochDuration = time.Second * 60 - TWAPPruningKeepPeriod = EpochDuration / 4 + EpochDayDuration = time.Second * 60 + EpochWeekDuration = time.Second * 120 + TWAPPruningKeepPeriod = EpochDayDuration / 4 ) var ( @@ -84,7 +89,7 @@ var ( StakeAmountIntB = sdk.NewInt(StakeAmountB) StakeAmountCoinB = sdk.NewCoin(OsmoDenom, StakeAmountIntB) - InitBalanceStrA = fmt.Sprintf("%d%s,%d%s,%d%s", OsmoBalanceA, OsmoDenom, StakeBalanceA, StakeDenom, IonBalanceA, IonDenom) + InitBalanceStrA = fmt.Sprintf("%d%s,%d%s,%d%s,%d%s,%d%s", OsmoBalanceA, OsmoDenom, StakeBalanceA, StakeDenom, IonBalanceA, IonDenom, UstBalanceA, UstIBCDenom, LuncBalanceA, LuncIBCDenom) InitBalanceStrB = fmt.Sprintf("%d%s,%d%s,%d%s", OsmoBalanceB, OsmoDenom, StakeBalanceB, StakeDenom, IonBalanceB, IonDenom) OsmoToken = sdk.NewInt64Coin(OsmoDenom, IbcSendAmount) // 3,300uosmo StakeToken = sdk.NewInt64Coin(StakeDenom, IbcSendAmount) // 3,300ustake @@ -308,7 +313,7 @@ func initGenesis(chain *internalChain, votingPeriod, expeditedVotingPeriod time. } func updateBankGenesis(bankGenState *banktypes.GenesisState) { - denomsToRegister := []string{StakeDenom, IonDenom, OsmoDenom, AtomDenom} + denomsToRegister := []string{StakeDenom, IonDenom, OsmoDenom, AtomDenom, LuncIBCDenom, UstIBCDenom} for _, denom := range denomsToRegister { setDenomMetadata(bankGenState, denom) } @@ -398,7 +403,8 @@ func updatePoolManagerGenesis(appGenState map[string]json.RawMessage) func(*pool func updateEpochGenesis(epochGenState *epochtypes.GenesisState) { epochGenState.Epochs = []epochtypes.EpochInfo{ - epochtypes.NewGenesisEpochInfo("week", time.Hour*24*7), + // override week epochs which are in default integrations, to be 2min + epochtypes.NewGenesisEpochInfo("week", time.Second*120), // override day epochs which are in default integrations, to be 1min epochtypes.NewGenesisEpochInfo("day", time.Second*60), } diff --git a/tests/e2e/scripts/protorevPool1.json b/tests/e2e/scripts/protorevPool1.json new file mode 100644 index 00000000000..ac3a1bd7af8 --- /dev/null +++ b/tests/e2e/scripts/protorevPool1.json @@ -0,0 +1,7 @@ +{ + "weights": "1ibc/BE1BB42D4BE3C30D50B68D7C41DB4DFCE9678E8EF8C539F6E6A9345048894FCC,1uosmo", + "initial-deposit": "18986995439401ibc/BE1BB42D4BE3C30D50B68D7C41DB4DFCE9678E8EF8C539F6E6A9345048894FCC,191801648570uosmo", + "swap-fee": "0.002", + "exit-fee": "0.0", + "future-governor": "" +} \ No newline at end of file diff --git a/tests/e2e/scripts/protorevPool2.json b/tests/e2e/scripts/protorevPool2.json new file mode 100644 index 00000000000..212b273361c --- /dev/null +++ b/tests/e2e/scripts/protorevPool2.json @@ -0,0 +1,7 @@ +{ + "weights": "1ibc/0EF15DF2F02480ADE0BB6E85D9EBB5DAEA2836D3860E9F97F9AADE4F57A31AA0,1ibc/BE1BB42D4BE3C30D50B68D7C41DB4DFCE9678E8EF8C539F6E6A9345048894FCC", + "initial-deposit": "72765460003038ibc/0EF15DF2F02480ADE0BB6E85D9EBB5DAEA2836D3860E9F97F9AADE4F57A31AA0,596032233203ibc/BE1BB42D4BE3C30D50B68D7C41DB4DFCE9678E8EF8C539F6E6A9345048894FCC", + "swap-fee": "0.00535", + "exit-fee": "0.0", + "future-governor": "" +} \ No newline at end of file diff --git a/tests/e2e/scripts/protorevPool3.json b/tests/e2e/scripts/protorevPool3.json new file mode 100644 index 00000000000..8f5a7687a40 --- /dev/null +++ b/tests/e2e/scripts/protorevPool3.json @@ -0,0 +1,7 @@ +{ + "weights": "1ibc/0EF15DF2F02480ADE0BB6E85D9EBB5DAEA2836D3860E9F97F9AADE4F57A31AA0,1uosmo", + "initial-deposit": "165624820984787ibc/0EF15DF2F02480ADE0BB6E85D9EBB5DAEA2836D3860E9F97F9AADE4F57A31AA0,13901565323uosmo", + "swap-fee": "0.002", + "exit-fee": "0.0", + "future-governor": "" +} \ No newline at end of file diff --git a/x/protorev/genesis.go b/x/protorev/genesis.go index 68b4c419598..aac0420e445 100644 --- a/x/protorev/genesis.go +++ b/x/protorev/genesis.go @@ -42,32 +42,32 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) } k.SetPoolWeights(ctx, poolWeights) - // Configure the initial base denoms used for cyclic route building - baseDenomPriorities := []*types.BaseDenom{ + // Configure the initial base denoms used for cyclic route building. The order of the list of base + // denoms is the order in which routes will be prioritized i.e. routes will be built and simulated in a + // first come first serve basis that is based on the order of the base denoms. + baseDenoms := []*types.BaseDenom{ { Denom: types.OsmosisDenomination, StepSize: sdk.NewInt(1_000_000), }, } - if err := k.SetBaseDenoms(ctx, baseDenomPriorities); err != nil { + if err := k.SetBaseDenoms(ctx, baseDenoms); err != nil { panic(err) } - // Update the pools on genesis - if err := k.UpdatePools(ctx); err != nil { + // Currently configured to be the Skip dev team's address + // See https://github.com/osmosis-labs/osmosis/issues/4349 for more details + // Note that governance has full ability to change this live on-chain, and this admin can at most prevent protorev from working. + // All the settings manager's controls have limits, so it can't lead to a chain halt, excess processing time or prevention of swaps. + adminAccount, err := sdk.AccAddressFromBech32("osmo17nv67dvc7f8yr00rhgxd688gcn9t9wvhn783z4") + if err != nil { panic(err) } + k.SetAdminAccount(ctx, adminAccount) - // Init all of the searcher routes - for _, tokenPairArbRoutes := range genState.TokenPairs { - err := tokenPairArbRoutes.Validate() - if err != nil { - panic(err) - } - - if err := k.SetTokenPairArbRoutes(ctx, tokenPairArbRoutes.TokenIn, tokenPairArbRoutes.TokenOut, &tokenPairArbRoutes); err != nil { - panic(err) - } + // Update the pools on genesis + if err := k.UpdatePools(ctx); err != nil { + panic(err) } } diff --git a/x/protorev/keeper/developer_fees_test.go b/x/protorev/keeper/developer_fees_test.go index fe119e31f9f..e0fc4ec7995 100644 --- a/x/protorev/keeper/developer_fees_test.go +++ b/x/protorev/keeper/developer_fees_test.go @@ -44,11 +44,11 @@ func (suite *KeeperTestSuite) TestSendDeveloperFeesToDeveloperAccount() { suite.Require().NoError(err) // Trade 2 - err = suite.pseudoExecuteTrade(types.AtomDenomination, sdk.NewInt(2000), 0) + err = suite.pseudoExecuteTrade("Atom", sdk.NewInt(2000), 0) suite.Require().NoError(err) }, expectedErr: false, - expectedCoins: sdk.NewCoins(sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(400)), sdk.NewCoin(types.AtomDenomination, sdk.NewInt(400))), + expectedCoins: sdk.NewCoins(sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(400)), sdk.NewCoin("Atom", sdk.NewInt(400))), }, { description: "Send with set developer account (after multiple trades across epochs)", @@ -61,7 +61,7 @@ func (suite *KeeperTestSuite) TestSendDeveloperFeesToDeveloperAccount() { suite.Require().NoError(err) // Trade 2 - err = suite.pseudoExecuteTrade(types.AtomDenomination, sdk.NewInt(2000), 0) + err = suite.pseudoExecuteTrade("Atom", sdk.NewInt(2000), 0) suite.Require().NoError(err) // Trade 3 after year 1 @@ -73,7 +73,7 @@ func (suite *KeeperTestSuite) TestSendDeveloperFeesToDeveloperAccount() { suite.Require().NoError(err) }, expectedErr: false, - expectedCoins: sdk.NewCoins(sdk.NewCoin(types.AtomDenomination, sdk.NewInt(400)), sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(700))), + expectedCoins: sdk.NewCoins(sdk.NewCoin("Atom", sdk.NewInt(400)), sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(700))), }, } diff --git a/x/protorev/keeper/epoch_hook_test.go b/x/protorev/keeper/epoch_hook_test.go index e45e90c18b7..755103afafc 100644 --- a/x/protorev/keeper/epoch_hook_test.go +++ b/x/protorev/keeper/epoch_hook_test.go @@ -4,9 +4,28 @@ import ( "fmt" "strings" + "testing" + "github.com/osmosis-labs/osmosis/v14/x/protorev/types" ) +// BenchmarkEpochHook benchmarks the epoch hook. In particular, it benchmarks the UpdatePools function. +func BenchmarkEpochHook(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + // Setup the test suite + suite := new(KeeperTestSuite) + suite.SetT(&testing.T{}) + suite.SetupTest() + + for i := 0; i < b.N; i++ { + b.StartTimer() + suite.App.ProtoRevKeeper.UpdatePools(suite.Ctx) + b.StopTimer() + } +} + // TestEpochHook tests that the epoch hook is correctly setting the pool IDs for all base denoms. Base denoms are the denoms that will // be used for cyclic arbitrage and must be stored in the keeper. The epoch hook is run after the pools are set and committed in keeper_test.go. // All of the pools are initialized in the setup function in keeper_test.go and are available in the suite.pools variable. In this test diff --git a/x/protorev/keeper/grpc_query_test.go b/x/protorev/keeper/grpc_query_test.go index 37830f48403..fd97c4f95d2 100644 --- a/x/protorev/keeper/grpc_query_test.go +++ b/x/protorev/keeper/grpc_query_test.go @@ -68,7 +68,7 @@ func (suite *KeeperTestSuite) TestGetProtoRevProfitsByDenom() { suite.Require().Equal(sdk.NewInt(10000), res.Profit.Amount) // Pseudo execute a trade in a different denom - err = suite.App.AppKeepers.ProtoRevKeeper.UpdateStatistics(suite.Ctx, poolmanagertypes.SwapAmountInRoutes{}, types.AtomDenomination, sdk.NewInt(10000)) + err = suite.App.AppKeepers.ProtoRevKeeper.UpdateStatistics(suite.Ctx, poolmanagertypes.SwapAmountInRoutes{}, "Atom", sdk.NewInt(10000)) suite.Require().NoError(err) suite.Commit() @@ -76,12 +76,12 @@ func (suite *KeeperTestSuite) TestGetProtoRevProfitsByDenom() { res, err = suite.queryClient.GetProtoRevProfitsByDenom(sdk.WrapSDKContext(suite.Ctx), req) suite.Require().NoError(err) req = &types.QueryGetProtoRevProfitsByDenomRequest{ - Denom: types.AtomDenomination, + Denom: "Atom", } res, err = suite.queryClient.GetProtoRevProfitsByDenom(sdk.WrapSDKContext(suite.Ctx), req) suite.Require().NoError(err) suite.Require().Equal(sdk.NewInt(10000), res.Profit.Amount) - suite.Require().Equal(types.AtomDenomination, res.Profit.Denom) + suite.Require().Equal("Atom", res.Profit.Denom) } // TestGetProtoRevAllProfits tests the query for all profits @@ -94,12 +94,12 @@ func (suite *KeeperTestSuite) TestGetProtoRevAllProfits() { // Pseudo execute a trade err = suite.App.AppKeepers.ProtoRevKeeper.UpdateStatistics(suite.Ctx, poolmanagertypes.SwapAmountInRoutes{}, types.OsmosisDenomination, sdk.NewInt(9000)) suite.Require().NoError(err) - err = suite.App.AppKeepers.ProtoRevKeeper.UpdateStatistics(suite.Ctx, poolmanagertypes.SwapAmountInRoutes{}, types.AtomDenomination, sdk.NewInt(3000)) + err = suite.App.AppKeepers.ProtoRevKeeper.UpdateStatistics(suite.Ctx, poolmanagertypes.SwapAmountInRoutes{}, "Atom", sdk.NewInt(3000)) suite.Require().NoError(err) res, err = suite.queryClient.GetProtoRevAllProfits(sdk.WrapSDKContext(suite.Ctx), req) suite.Require().NoError(err) - atom := sdk.NewCoin(types.AtomDenomination, sdk.NewInt(3000)) + atom := sdk.NewCoin("Atom", sdk.NewInt(3000)) osmo := sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(9000)) suite.Require().Contains(res.Profits, &atom) suite.Require().Contains(res.Profits, &osmo) @@ -107,12 +107,12 @@ func (suite *KeeperTestSuite) TestGetProtoRevAllProfits() { // Pseudo execute more trades err = suite.App.AppKeepers.ProtoRevKeeper.UpdateStatistics(suite.Ctx, poolmanagertypes.SwapAmountInRoutes{}, types.OsmosisDenomination, sdk.NewInt(10000)) suite.Require().NoError(err) - err = suite.App.AppKeepers.ProtoRevKeeper.UpdateStatistics(suite.Ctx, poolmanagertypes.SwapAmountInRoutes{}, types.AtomDenomination, sdk.NewInt(10000)) + err = suite.App.AppKeepers.ProtoRevKeeper.UpdateStatistics(suite.Ctx, poolmanagertypes.SwapAmountInRoutes{}, "Atom", sdk.NewInt(10000)) suite.Require().NoError(err) res, err = suite.queryClient.GetProtoRevAllProfits(sdk.WrapSDKContext(suite.Ctx), req) suite.Require().NoError(err) - atom = sdk.NewCoin(types.AtomDenomination, sdk.NewInt(13000)) + atom = sdk.NewCoin("Atom", sdk.NewInt(13000)) osmo = sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(19000)) suite.Require().Contains(res.Profits, &atom) suite.Require().Contains(res.Profits, &osmo) @@ -130,7 +130,7 @@ func (suite *KeeperTestSuite) TestGetProtoRevStatisticsByRoute() { suite.Require().Nil(res) // Pseudo execute a trade - err = suite.App.AppKeepers.ProtoRevKeeper.UpdateStatistics(suite.Ctx, poolmanagertypes.SwapAmountInRoutes{{TokenOutDenom: "", PoolId: 1}, {TokenOutDenom: "", PoolId: 2}, {TokenOutDenom: "", PoolId: 3}}, types.AtomDenomination, sdk.NewInt(10000)) + err = suite.App.AppKeepers.ProtoRevKeeper.UpdateStatistics(suite.Ctx, poolmanagertypes.SwapAmountInRoutes{{TokenOutDenom: "", PoolId: 1}, {TokenOutDenom: "", PoolId: 2}, {TokenOutDenom: "", PoolId: 3}}, "Atom", sdk.NewInt(10000)) suite.Require().NoError(err) // Verify statistics @@ -138,11 +138,11 @@ func (suite *KeeperTestSuite) TestGetProtoRevStatisticsByRoute() { suite.Require().NoError(err) suite.Require().Equal([]uint64{1, 2, 3}, res.Statistics.Route) suite.Require().Equal(sdk.OneInt(), res.Statistics.NumberOfTrades) - coin := sdk.NewCoin(types.AtomDenomination, sdk.NewInt(10000)) + coin := sdk.NewCoin("Atom", sdk.NewInt(10000)) suite.Require().Contains(res.Statistics.Profits, &coin) // Pseudo execute another trade - err = suite.App.AppKeepers.ProtoRevKeeper.UpdateStatistics(suite.Ctx, poolmanagertypes.SwapAmountInRoutes{{TokenOutDenom: "", PoolId: 1}, {TokenOutDenom: "", PoolId: 2}, {TokenOutDenom: "", PoolId: 3}}, types.AtomDenomination, sdk.NewInt(80000)) + err = suite.App.AppKeepers.ProtoRevKeeper.UpdateStatistics(suite.Ctx, poolmanagertypes.SwapAmountInRoutes{{TokenOutDenom: "", PoolId: 1}, {TokenOutDenom: "", PoolId: 2}, {TokenOutDenom: "", PoolId: 3}}, "Atom", sdk.NewInt(80000)) suite.Require().NoError(err) // Verify statistics @@ -150,7 +150,7 @@ func (suite *KeeperTestSuite) TestGetProtoRevStatisticsByRoute() { suite.Require().NoError(err) suite.Require().Equal([]uint64{1, 2, 3}, res.Statistics.Route) suite.Require().Equal(sdk.NewInt(2), res.Statistics.NumberOfTrades) - coin = sdk.NewCoin(types.AtomDenomination, sdk.NewInt(90000)) + coin = sdk.NewCoin("Atom", sdk.NewInt(90000)) suite.Require().Contains(res.Statistics.Profits, &coin) // Pseudo execute another trade in a different denom (might happen in multidenom pools > 2 denoms) @@ -162,7 +162,7 @@ func (suite *KeeperTestSuite) TestGetProtoRevStatisticsByRoute() { suite.Require().NoError(err) suite.Require().Equal([]uint64{1, 2, 3}, res.Statistics.Route) suite.Require().Equal(sdk.NewInt(3), res.Statistics.NumberOfTrades) - atomCoin := sdk.NewCoin(types.AtomDenomination, sdk.NewInt(90000)) + atomCoin := sdk.NewCoin("Atom", sdk.NewInt(90000)) osmoCoin := sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(80000)) suite.Require().Contains(res.Statistics.Profits, &atomCoin) suite.Require().Contains(res.Statistics.Profits, &osmoCoin) @@ -220,7 +220,7 @@ func (suite *KeeperTestSuite) TestGetProtoRevAllRouteStatistics() { suite.Require().Contains(res.Statistics[1].Profits, &osmoCoin) // Pseudo execute another trade on a different route and denom - err = suite.App.AppKeepers.ProtoRevKeeper.UpdateStatistics(suite.Ctx, poolmanagertypes.SwapAmountInRoutes{{TokenOutDenom: "", PoolId: 5}, {TokenOutDenom: "", PoolId: 2}, {TokenOutDenom: "", PoolId: 4}}, types.AtomDenomination, sdk.NewInt(80000)) + err = suite.App.AppKeepers.ProtoRevKeeper.UpdateStatistics(suite.Ctx, poolmanagertypes.SwapAmountInRoutes{{TokenOutDenom: "", PoolId: 5}, {TokenOutDenom: "", PoolId: 2}, {TokenOutDenom: "", PoolId: 4}}, "Atom", sdk.NewInt(80000)) suite.Require().NoError(err) // Verify statistics @@ -239,7 +239,7 @@ func (suite *KeeperTestSuite) TestGetProtoRevAllRouteStatistics() { suite.Require().Equal([]uint64{5, 2, 4}, res.Statistics[2].Route) suite.Require().Equal(sdk.OneInt(), res.Statistics[2].NumberOfTrades) - atomCoin := sdk.NewCoin(types.AtomDenomination, sdk.NewInt(80000)) + atomCoin := sdk.NewCoin("Atom", sdk.NewInt(80000)) suite.Require().Contains(res.Statistics[2].Profits, &atomCoin) } @@ -326,18 +326,8 @@ func (suite *KeeperTestSuite) TestGetProtoRevMaxPoolPointsPerBlock() { // TestGetProtoRevBaseDenoms tests the query to retrieve the base denoms func (suite *KeeperTestSuite) TestGetProtoRevBaseDenoms() { - // Set the base denoms - baseDenoms := []*types.BaseDenom{ - { - Denom: types.OsmosisDenomination, - StepSize: sdk.NewInt(1000000000000000000), - }, - { - Denom: types.AtomDenomination, - StepSize: sdk.NewInt(1000000000000000000), - }, - } - err := suite.App.AppKeepers.ProtoRevKeeper.SetBaseDenoms(suite.Ctx, baseDenoms) + // base denoms already set in setup + baseDenoms, err := suite.App.AppKeepers.ProtoRevKeeper.GetAllBaseDenoms(suite.Ctx) suite.Require().NoError(err) req := &types.QueryGetProtoRevBaseDenomsRequest{} diff --git a/x/protorev/keeper/keeper_test.go b/x/protorev/keeper/keeper_test.go index bf5a873c84d..8a4fba2e64b 100644 --- a/x/protorev/keeper/keeper_test.go +++ b/x/protorev/keeper/keeper_test.go @@ -89,7 +89,11 @@ func (suite *KeeperTestSuite) SetupTest() { StepSize: sdk.NewInt(1_000_000), }, { - Denom: types.AtomDenomination, + Denom: "Atom", + StepSize: sdk.NewInt(1_000_000), + }, + { + Denom: "test/3", StepSize: sdk.NewInt(1_000_000), }, } @@ -105,7 +109,7 @@ func (suite *KeeperTestSuite) SetupTest() { // Set default configuration for testing suite.balances = sdk.NewCoins( sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(9000000000000000000)), - sdk.NewCoin(types.AtomDenomination, sdk.NewInt(9000000000000000000)), + sdk.NewCoin("Atom", sdk.NewInt(9000000000000000000)), sdk.NewCoin("akash", sdk.NewInt(9000000000000000000)), sdk.NewCoin("bitcoin", sdk.NewInt(9000000000000000000)), sdk.NewCoin("canto", sdk.NewInt(9000000000000000000)), @@ -120,6 +124,11 @@ func (suite *KeeperTestSuite) SetupTest() { sdk.NewCoin("usdt", sdk.NewInt(9000000000000000000)), sdk.NewCoin("busd", sdk.NewInt(9000000000000000000)), sdk.NewCoin("ibc/A0CC0CF735BFB30E730C70019D4218A1244FF383503FF7579C9201AB93CA9293", sdk.NewInt(9000000000000000000)), + sdk.NewCoin("test/1", sdk.NewInt(9000000000000000000)), + sdk.NewCoin("test/2", sdk.NewInt(9000000000000000000)), + sdk.NewCoin("test/3", sdk.NewInt(9000000000000000000)), + sdk.NewCoin("usdx", sdk.NewInt(9000000000000000000)), + sdk.NewCoin("usdy", sdk.NewInt(9000000000000000000)), ) suite.fundAllAccountsWith() suite.Commit() @@ -164,7 +173,7 @@ func (suite *KeeperTestSuite) setUpPools() { Weight: sdk.NewInt(1), }, { - Token: sdk.NewCoin(types.AtomDenomination, sdk.NewInt(1000)), + Token: sdk.NewCoin("Atom", sdk.NewInt(1000)), Weight: sdk.NewInt(1), }, }, @@ -179,7 +188,7 @@ func (suite *KeeperTestSuite) setUpPools() { Weight: sdk.NewInt(1), }, { - Token: sdk.NewCoin(types.AtomDenomination, sdk.NewInt(1000)), + Token: sdk.NewCoin("Atom", sdk.NewInt(1000)), Weight: sdk.NewInt(1), }, }, @@ -194,7 +203,7 @@ func (suite *KeeperTestSuite) setUpPools() { Weight: sdk.NewInt(1), }, { - Token: sdk.NewCoin(types.AtomDenomination, sdk.NewInt(1000)), + Token: sdk.NewCoin("Atom", sdk.NewInt(1000)), Weight: sdk.NewInt(1), }, }, @@ -209,7 +218,7 @@ func (suite *KeeperTestSuite) setUpPools() { Weight: sdk.NewInt(1), }, { - Token: sdk.NewCoin(types.AtomDenomination, sdk.NewInt(1000)), + Token: sdk.NewCoin("Atom", sdk.NewInt(1000)), Weight: sdk.NewInt(1), }, }, @@ -224,7 +233,7 @@ func (suite *KeeperTestSuite) setUpPools() { Weight: sdk.NewInt(1), }, { - Token: sdk.NewCoin(types.AtomDenomination, sdk.NewInt(1000)), + Token: sdk.NewCoin("Atom", sdk.NewInt(1000)), Weight: sdk.NewInt(1), }, }, @@ -239,7 +248,7 @@ func (suite *KeeperTestSuite) setUpPools() { Weight: sdk.NewInt(1), }, { - Token: sdk.NewCoin(types.AtomDenomination, sdk.NewInt(1000)), + Token: sdk.NewCoin("Atom", sdk.NewInt(1000)), Weight: sdk.NewInt(1), }, }, @@ -520,7 +529,7 @@ func (suite *KeeperTestSuite) setUpPools() { { // Pool 25 PoolAssets: []balancertypes.PoolAsset{ { - Token: sdk.NewCoin(types.AtomDenomination, sdk.NewInt(165624820984787)), + Token: sdk.NewCoin("Atom", sdk.NewInt(165624820984787)), Weight: sdk.NewInt(1), }, { @@ -630,7 +639,7 @@ func (suite *KeeperTestSuite) setUpPools() { Weight: sdk.NewInt(25), }, { - Token: sdk.NewCoin(types.AtomDenomination, sdk.NewInt(6121181710)), + Token: sdk.NewCoin("Atom", sdk.NewInt(6121181710)), Weight: sdk.NewInt(25), }, }, @@ -660,7 +669,7 @@ func (suite *KeeperTestSuite) setUpPools() { Weight: sdk.NewInt(70), }, { - Token: sdk.NewCoin(types.AtomDenomination, sdk.NewInt(10285796639)), + Token: sdk.NewCoin("Atom", sdk.NewInt(10285796639)), Weight: sdk.NewInt(30), }, }, @@ -668,6 +677,96 @@ func (suite *KeeperTestSuite) setUpPools() { ExitFee: sdk.NewDecWithPrec(0, 2), PoolId: 33, }, + { // Pool 34 + PoolAssets: []balancertypes.PoolAsset{ + { + Token: sdk.NewCoin("Atom", sdk.NewInt(364647340206)), + Weight: sdk.NewInt(1), + }, + { + Token: sdk.NewCoin("test/1", sdk.NewInt(1569764554938)), + Weight: sdk.NewInt(1), + }, + }, + SwapFee: sdk.NewDecWithPrec(3, 3), + ExitFee: sdk.NewDecWithPrec(0, 2), + PoolId: 34, + }, + { // Pool 35 + PoolAssets: []balancertypes.PoolAsset{ + { + Token: sdk.NewCoin("test/1", sdk.NewInt(1026391517901)), + Weight: sdk.NewInt(1), + }, + { + Token: sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(1694086377216)), + Weight: sdk.NewInt(1), + }, + }, + SwapFee: sdk.NewDecWithPrec(2, 3), + ExitFee: sdk.NewDecWithPrec(0, 2), + PoolId: 35, + }, + { // Pool 36 + PoolAssets: []balancertypes.PoolAsset{ + { + Token: sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(2774812791932)), + Weight: sdk.NewInt(1), + }, + { + Token: sdk.NewCoin("test/2", sdk.NewInt(1094837653970)), + Weight: sdk.NewInt(1), + }, + }, + SwapFee: sdk.NewDecWithPrec(3, 3), + ExitFee: sdk.NewDecWithPrec(0, 2), + PoolId: 36, + }, + { // Pool 37 + PoolAssets: []balancertypes.PoolAsset{ + { + Token: sdk.NewCoin("Atom", sdk.NewInt(406165719545)), + Weight: sdk.NewInt(1), + }, + { + Token: sdk.NewCoin("test/2", sdk.NewInt(1095887931673)), + Weight: sdk.NewInt(1), + }, + }, + SwapFee: sdk.NewDecWithPrec(3, 3), + ExitFee: sdk.NewDecWithPrec(0, 2), + PoolId: 37, + }, + { // Pool 38 + PoolAssets: []balancertypes.PoolAsset{ + { + Token: sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(6111815027)), + Weight: sdk.NewInt(1), + }, + { + Token: sdk.NewCoin("test/3", sdk.NewInt(4478366578)), + Weight: sdk.NewInt(1), + }, + }, + SwapFee: sdk.NewDecWithPrec(2, 3), + ExitFee: sdk.NewDecWithPrec(0, 2), + PoolId: 38, + }, + { // Pool 39 + PoolAssets: []balancertypes.PoolAsset{ + { + Token: sdk.NewCoin("test/3", sdk.NewInt(18631000485558)), + Weight: sdk.NewInt(1), + }, + { + Token: sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(17000185817963)), + Weight: sdk.NewInt(1), + }, + }, + SwapFee: sdk.NewDecWithPrec(2, 3), + ExitFee: sdk.NewDecWithPrec(0, 2), + PoolId: 39, + }, } for _, pool := range suite.pools { @@ -675,7 +774,7 @@ func (suite *KeeperTestSuite) setUpPools() { } suite.stableSwapPools = []StableSwapPool{ - { // Pool 34 + { // Pool 40 initialLiquidity: sdk.NewCoins( sdk.NewCoin("usdc", sdk.NewInt(1000000000000000)), sdk.NewCoin("usdt", sdk.NewInt(1000000000000000)), @@ -686,7 +785,64 @@ func (suite *KeeperTestSuite) setUpPools() { ExitFee: sdk.NewDecWithPrec(0, 2), }, scalingFactors: []uint64{1, 1, 1}, - }} + }, + { // Pool 41 - Used for doomsday testing + initialLiquidity: sdk.NewCoins( + sdk.NewCoin("usdc", sdk.NewInt(1000000000000000)), + sdk.NewCoin("usdt", sdk.NewInt(1000000000000000)), + sdk.NewCoin("busd", sdk.NewInt(2000000000000000)), + ), + poolParams: stableswap.PoolParams{ + SwapFee: sdk.NewDecWithPrec(1, 4), + ExitFee: sdk.NewDecWithPrec(0, 2), + }, + scalingFactors: []uint64{1, 1, 1}, + }, + { // Pool 42 - Used for extended range testing + initialLiquidity: sdk.NewCoins( + sdk.NewCoin("usdx", sdk.NewInt(1000000000000000)), + sdk.NewCoin("usdy", sdk.NewInt(2000000000000000)), + ), + poolParams: stableswap.PoolParams{ + SwapFee: sdk.NewDecWithPrec(1, 4), + ExitFee: sdk.NewDecWithPrec(0, 2), + }, + scalingFactors: []uint64{1, 1}, + }, + { // Pool 43 - Used for extended range testing + initialLiquidity: sdk.NewCoins( + sdk.NewCoin("usdx", sdk.NewInt(2000000000000000)), + sdk.NewCoin("usdy", sdk.NewInt(1000000000000000)), + ), + poolParams: stableswap.PoolParams{ + SwapFee: sdk.NewDecWithPrec(1, 4), + ExitFee: sdk.NewDecWithPrec(0, 2), + }, + scalingFactors: []uint64{1, 1}, + }, + { // Pool 44 - Used for panic catching testing + initialLiquidity: sdk.NewCoins( + sdk.NewCoin("usdx", sdk.NewInt(1000)), + sdk.NewCoin("usdy", sdk.NewInt(2000)), + ), + poolParams: stableswap.PoolParams{ + SwapFee: sdk.NewDecWithPrec(1, 4), + ExitFee: sdk.NewDecWithPrec(0, 2), + }, + scalingFactors: []uint64{1, 1}, + }, + { // Pool 45 - Used for panic catching testing + initialLiquidity: sdk.NewCoins( + sdk.NewCoin("usdx", sdk.NewInt(2000)), + sdk.NewCoin("usdy", sdk.NewInt(1000)), + ), + poolParams: stableswap.PoolParams{ + SwapFee: sdk.NewDecWithPrec(1, 4), + ExitFee: sdk.NewDecWithPrec(0, 2), + }, + scalingFactors: []uint64{1, 1}, + }, + } for _, pool := range suite.stableSwapPools { suite.createStableswapPool(pool.initialLiquidity, pool.poolParams, pool.scalingFactors) @@ -738,24 +894,42 @@ func (suite *KeeperTestSuite) fundAllAccountsWith() { // setUpTokenPairRoutes sets up the searcher routes for testing func (suite *KeeperTestSuite) setUpTokenPairRoutes() { // General Test Route - atomAkash := types.NewTrade(0, types.AtomDenomination, "akash") + atomAkash := types.NewTrade(0, "Atom", "akash") akashBitcoin := types.NewTrade(14, "akash", "bitcoin") - atomBitcoin := types.NewTrade(4, "bitcoin", types.AtomDenomination) + atomBitcoin := types.NewTrade(4, "bitcoin", "Atom") // Stableswap Route uosmoUSDC := types.NewTrade(0, types.OsmosisDenomination, "usdc") - usdcBUSD := types.NewTrade(34, "usdc", "busd") + usdcBUSD := types.NewTrade(40, "usdc", "busd") busdUOSMO := types.NewTrade(30, "busd", types.OsmosisDenomination) // Atom Route - atomIBC1 := types.NewTrade(31, types.AtomDenomination, "ibc/BE1BB42D4BE3C30D50B68D7C41DB4DFCE9678E8EF8C539F6E6A9345048894FCC") + atomIBC1 := types.NewTrade(31, "Atom", "ibc/BE1BB42D4BE3C30D50B68D7C41DB4DFCE9678E8EF8C539F6E6A9345048894FCC") ibc1IBC2 := types.NewTrade(32, "ibc/BE1BB42D4BE3C30D50B68D7C41DB4DFCE9678E8EF8C539F6E6A9345048894FCC", "ibc/A0CC0CF735BFB30E730C70019D4218A1244FF383503FF7579C9201AB93CA9293") - ibc2ATOM := types.NewTrade(0, "ibc/A0CC0CF735BFB30E730C70019D4218A1244FF383503FF7579C9201AB93CA9293", types.AtomDenomination) + ibc2ATOM := types.NewTrade(0, "ibc/A0CC0CF735BFB30E730C70019D4218A1244FF383503FF7579C9201AB93CA9293", "Atom") + + // Four-Pool Route + fourPool0 := types.NewTrade(34, "Atom", "test/1") + fourPool1 := types.NewTrade(35, "test/1", types.OsmosisDenomination) + fourPool2 := types.NewTrade(36, types.OsmosisDenomination, "test/2") + fourPool3 := types.NewTrade(0, "test/2", "Atom") + + // Two-Pool Route + twoPool0 := types.NewTrade(0, "test/3", types.OsmosisDenomination) + twoPool1 := types.NewTrade(39, types.OsmosisDenomination, "test/3") + + // Doomsday Route - Stableswap + doomsdayStable0 := types.NewTrade(29, types.OsmosisDenomination, "usdc") + doomsdayStable1 := types.NewTrade(0, "usdc", "busd") + doomsdayStable2 := types.NewTrade(30, "busd", types.OsmosisDenomination) + + standardStepSize := sdk.NewInt(1_000_000) suite.tokenPairArbRoutes = []*types.TokenPairArbRoutes{ { TokenIn: "akash", - TokenOut: types.AtomDenomination, + TokenOut: "Atom", + StepSize: &standardStepSize, ArbRoutes: []*types.Route{ { Trades: []*types.Trade{&atomAkash, &akashBitcoin, &atomBitcoin}, @@ -765,6 +939,7 @@ func (suite *KeeperTestSuite) setUpTokenPairRoutes() { { TokenIn: "usdc", TokenOut: types.OsmosisDenomination, + StepSize: &standardStepSize, ArbRoutes: []*types.Route{ { Trades: []*types.Trade{&uosmoUSDC, &usdcBUSD, &busdUOSMO}, @@ -772,14 +947,45 @@ func (suite *KeeperTestSuite) setUpTokenPairRoutes() { }, }, { - TokenIn: types.AtomDenomination, + TokenIn: "Atom", TokenOut: "ibc/A0CC0CF735BFB30E730C70019D4218A1244FF383503FF7579C9201AB93CA9293", + StepSize: &standardStepSize, ArbRoutes: []*types.Route{ { Trades: []*types.Trade{&atomIBC1, &ibc1IBC2, &ibc2ATOM}, }, }, }, + { + TokenIn: "Atom", + TokenOut: "test/2", + StepSize: &standardStepSize, + ArbRoutes: []*types.Route{ + { + Trades: []*types.Trade{&fourPool0, &fourPool1, &fourPool2, &fourPool3}, + }, + }, + }, + { + TokenIn: types.OsmosisDenomination, + TokenOut: "test/3", + StepSize: &standardStepSize, + ArbRoutes: []*types.Route{ + { + Trades: []*types.Trade{&twoPool0, &twoPool1}, + }, + }, + }, + { + TokenIn: "busd", + TokenOut: "usdc", + StepSize: &standardStepSize, + ArbRoutes: []*types.Route{ + { + Trades: []*types.Trade{&doomsdayStable0, &doomsdayStable1, &doomsdayStable2}, + }, + }, + }, } for _, tokenPair := range suite.tokenPairArbRoutes { diff --git a/x/protorev/keeper/msg_server.go b/x/protorev/keeper/msg_server.go index c443c8268c7..2d593a744b0 100644 --- a/x/protorev/keeper/msg_server.go +++ b/x/protorev/keeper/msg_server.go @@ -162,7 +162,7 @@ func (m MsgServer) AdminCheck(ctx sdk.Context, admin string) error { // Ensure the admin and sender are the same if !adminAccount.Equals(sender) { - return fmt.Errorf("sender account %s is not authorized to set base denoms. sender must be %s", sender.String(), adminAccount.String()) + return fmt.Errorf("sender account %s is not authorized. sender must be %s", sender.String(), adminAccount.String()) } return nil diff --git a/x/protorev/keeper/msg_server_test.go b/x/protorev/keeper/msg_server_test.go index 909745fffe2..65683afdf35 100644 --- a/x/protorev/keeper/msg_server_test.go +++ b/x/protorev/keeper/msg_server_test.go @@ -10,6 +10,9 @@ import ( // TestMsgSetHotRoutes tests the MsgSetHotRoutes message. func (suite *KeeperTestSuite) TestMsgSetHotRoutes() { + validStepSize := sdk.NewInt(1_000_000) + invalidStepSize := sdk.NewInt(0) + testCases := []struct { description string admin string @@ -41,7 +44,7 @@ func (suite *KeeperTestSuite) TestMsgSetHotRoutes() { Trades: []*types.Trade{ { Pool: 1, - TokenIn: types.AtomDenomination, + TokenIn: "Atom", TokenOut: "Juno", }, { @@ -52,13 +55,14 @@ func (suite *KeeperTestSuite) TestMsgSetHotRoutes() { { Pool: 3, TokenIn: types.OsmosisDenomination, - TokenOut: types.AtomDenomination, + TokenOut: "Atom", }, }, }, }, TokenIn: types.OsmosisDenomination, TokenOut: "Juno", + StepSize: &validStepSize, }, }, true, @@ -74,7 +78,7 @@ func (suite *KeeperTestSuite) TestMsgSetHotRoutes() { Trades: []*types.Trade{ { Pool: 1, - TokenIn: types.AtomDenomination, + TokenIn: "Atom", TokenOut: "Juno", }, { @@ -85,13 +89,14 @@ func (suite *KeeperTestSuite) TestMsgSetHotRoutes() { { Pool: 3, TokenIn: types.OsmosisDenomination, - TokenOut: types.AtomDenomination, + TokenOut: "Atom", }, }, }, }, TokenIn: types.OsmosisDenomination, TokenOut: "Juno", + StepSize: &validStepSize, }, { ArbRoutes: []*types.Route{ @@ -99,7 +104,75 @@ func (suite *KeeperTestSuite) TestMsgSetHotRoutes() { Trades: []*types.Trade{ { Pool: 1, - TokenIn: types.AtomDenomination, + TokenIn: "Atom", + TokenOut: "Juno", + }, + { + Pool: 0, + TokenIn: "Juno", + TokenOut: types.OsmosisDenomination, + }, + { + Pool: 3, + TokenIn: types.OsmosisDenomination, + TokenOut: "Atom", + }, + }, + }, + }, + TokenIn: types.OsmosisDenomination, + TokenOut: "Juno", + StepSize: &validStepSize, + }, + }, + false, + false, + }, + { + "Invalid message (with proper hot routes)", + suite.adminAccount.String(), + []*types.TokenPairArbRoutes{ + { + ArbRoutes: []*types.Route{ + { + Trades: []*types.Trade{ + { + Pool: 1, + TokenIn: "Atom", + TokenOut: "Juno", + }, + { + Pool: 0, + TokenIn: "Juno", + TokenOut: types.OsmosisDenomination, + }, + { + Pool: 3, + TokenIn: types.OsmosisDenomination, + TokenOut: "Atom", + }, + }, + }, + }, + TokenIn: types.OsmosisDenomination, + TokenOut: "Juno", + StepSize: &invalidStepSize, + }, + }, + false, + false, + }, + { + "Invalid message with nil step size (with proper hot routes)", + suite.adminAccount.String(), + []*types.TokenPairArbRoutes{ + { + ArbRoutes: []*types.Route{ + { + Trades: []*types.Trade{ + { + Pool: 1, + TokenIn: "Atom", TokenOut: "Juno", }, { @@ -110,7 +183,7 @@ func (suite *KeeperTestSuite) TestMsgSetHotRoutes() { { Pool: 3, TokenIn: types.OsmosisDenomination, - TokenOut: types.AtomDenomination, + TokenOut: "Atom", }, }, }, @@ -505,7 +578,7 @@ func (suite *KeeperTestSuite) TestMsgSetBaseDenoms() { suite.adminAccount.String(), []*types.BaseDenom{ { - Denom: types.AtomDenomination, + Denom: "Atom", StepSize: sdk.NewInt(1_000_000), }, }, diff --git a/x/protorev/keeper/posthandler.go b/x/protorev/keeper/posthandler.go index 25802450841..d98739b6d66 100644 --- a/x/protorev/keeper/posthandler.go +++ b/x/protorev/keeper/posthandler.go @@ -5,6 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" + gammtypes "github.com/osmosis-labs/osmosis/v14/x/gamm/types" poolmanagertypes "github.com/osmosis-labs/osmosis/v14/x/poolmanager/types" ) @@ -99,18 +100,12 @@ func (k Keeper) AnteHandleCheck(ctx sdk.Context) error { // 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 pool points that can be consumed in this transaction - remainingPoolPoints, err := k.RemainingPoolPointsForTx(ctx) - if err != nil { - return err - } - // Iterate and build arbitrage routes for each pool that was swapped on - for index := 0; index < len(swappedPools) && *remainingPoolPoints > 0; index++ { + for _, pool := range swappedPools { // Build the routes for the pool that was swapped on - routes := k.BuildRoutes(ctx, swappedPools[index].TokenInDenom, swappedPools[index].TokenOutDenom, swappedPools[index].PoolId, remainingPoolPoints) + routes := k.BuildRoutes(ctx, pool.TokenInDenom, pool.TokenOutDenom, pool.PoolId) - // Find optimal input amounts for routes + // Find optimal route (input coin, profit, route) for the given 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. @@ -131,22 +126,52 @@ func ExtractSwappedPools(tx sdk.Tx) []SwapToBackrun { // Extract only swaps types and the swapped pools from the tx for _, msg := range tx.GetMsgs() { - if swap, ok := msg.(*poolmanagertypes.MsgSwapExactAmountIn); ok { - for _, route := range swap.Routes { - swappedPools = append(swappedPools, SwapToBackrun{ - PoolId: route.PoolId, - TokenOutDenom: route.TokenOutDenom, - TokenInDenom: swap.TokenIn.Denom}) - } - } else if swap, ok := msg.(*poolmanagertypes.MsgSwapExactAmountOut); ok { - for _, route := range swap.Routes { - swappedPools = append(swappedPools, SwapToBackrun{ - PoolId: route.PoolId, - TokenOutDenom: swap.TokenOut.Denom, - TokenInDenom: route.TokenInDenom}) - } + switch msg := msg.(type) { + case *poolmanagertypes.MsgSwapExactAmountIn: + swappedPools = append(swappedPools, extractSwapInPools(msg.Routes, msg.TokenIn.Denom)...) + case *poolmanagertypes.MsgSwapExactAmountOut: + swappedPools = append(swappedPools, extractSwapOutPools(msg.Routes, msg.TokenOut.Denom)...) + case *gammtypes.MsgSwapExactAmountIn: + swappedPools = append(swappedPools, extractSwapInPools(msg.Routes, msg.TokenIn.Denom)...) + case *gammtypes.MsgSwapExactAmountOut: + swappedPools = append(swappedPools, extractSwapOutPools(msg.Routes, msg.TokenOut.Denom)...) } } return swappedPools } + +// extractSwapInPools extracts the pools that were swapped on for a MsgSwapExactAmountIn +func extractSwapInPools(routes []poolmanagertypes.SwapAmountInRoute, tokenInDenom string) []SwapToBackrun { + swappedPools := make([]SwapToBackrun, 0) + + prevTokenIn := tokenInDenom + for _, route := range routes { + swappedPools = append(swappedPools, SwapToBackrun{ + PoolId: route.PoolId, + TokenOutDenom: route.TokenOutDenom, + TokenInDenom: prevTokenIn}) + + prevTokenIn = route.TokenOutDenom + } + + return swappedPools +} + +// extractSwapOutPools extracts the pools that were swapped on for a MsgSwapExactAmountOut +func extractSwapOutPools(routes []poolmanagertypes.SwapAmountOutRoute, tokenOutDenom string) []SwapToBackrun { + swappedPools := make([]SwapToBackrun, 0) + + prevTokenOut := tokenOutDenom + for i := len(routes) - 1; i >= 0; i-- { + route := routes[i] + swappedPools = append(swappedPools, SwapToBackrun{ + PoolId: route.PoolId, + TokenOutDenom: prevTokenOut, + TokenInDenom: route.TokenInDenom}) + + prevTokenOut = route.TokenInDenom + } + + return swappedPools +} diff --git a/x/protorev/keeper/posthandler_test.go b/x/protorev/keeper/posthandler_test.go index 10cbca0aa90..d6ca97ebff2 100644 --- a/x/protorev/keeper/posthandler_test.go +++ b/x/protorev/keeper/posthandler_test.go @@ -1,6 +1,9 @@ package keeper_test import ( + "strings" + "testing" + clienttx "github.com/cosmos/cosmos-sdk/client/tx" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" "github.com/cosmos/cosmos-sdk/simapp" @@ -8,11 +11,66 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + gammtypes "github.com/osmosis-labs/osmosis/v14/x/gamm/types" poolmanagertypes "github.com/osmosis-labs/osmosis/v14/x/poolmanager/types" "github.com/osmosis-labs/osmosis/v14/x/protorev/keeper" "github.com/osmosis-labs/osmosis/v14/x/protorev/types" ) +// BenchmarkBalancerSwapHighestLiquidityArb benchmarks a balancer swap that creates a single three hop arbitrage +// route with only balancer pools created by the highest liquidity method. +func BenchmarkBalancerSwapHighestLiquidityArb(b *testing.B) { + msgs := []sdk.Msg{ + &poolmanagertypes.MsgSwapExactAmountIn{ + Routes: []poolmanagertypes.SwapAmountInRoute{ + { + PoolId: 23, + TokenOutDenom: "ibc/BE1BB42D4BE3C30D50B68D7C41DB4DFCE9678E8EF8C539F6E6A9345048894FCC", + }, + }, + TokenIn: sdk.NewCoin("ibc/0EF15DF2F02480ADE0BB6E85D9EBB5DAEA2836D3860E9F97F9AADE4F57A31AA0", sdk.NewInt(10000)), + TokenOutMinAmount: sdk.NewInt(10000), + }, + } + benchmarkWrapper(b, msgs, 1) +} + +// BenchmarkStableSwapHotRouteArb benchmarks a balancer swap that gets back run by a single three hop arbitrage +// with a single stable pool and 2 balancer pools created via the hot routes method. +func BenchmarkStableSwapHotRouteArb(b *testing.B) { + msgs := []sdk.Msg{ + &poolmanagertypes.MsgSwapExactAmountIn{ + Routes: []poolmanagertypes.SwapAmountInRoute{ + { + PoolId: 29, + TokenOutDenom: types.OsmosisDenomination, + }, + }, + TokenIn: sdk.NewCoin("usdc", sdk.NewInt(10000)), + TokenOutMinAmount: sdk.NewInt(100), + }, + } + benchmarkWrapper(b, msgs, 1) +} + +// BenchmarkFourHopArb benchmarks a balancer swap that gets back run by a single four hop arbitrage route +// created via the hot routes method. +func BenchmarkFourHopHotRouteArb(b *testing.B) { + msgs := []sdk.Msg{ + &poolmanagertypes.MsgSwapExactAmountIn{ + Routes: []poolmanagertypes.SwapAmountInRoute{ + { + PoolId: 37, + TokenOutDenom: "test/2", + }, + }, + TokenIn: sdk.NewCoin("Atom", sdk.NewInt(10000)), + TokenOutMinAmount: sdk.NewInt(100), + }, + } + benchmarkWrapper(b, msgs, 1) +} + func (suite *KeeperTestSuite) TestAnteHandle() { type param struct { msgs []sdk.Msg @@ -77,7 +135,7 @@ func (suite *KeeperTestSuite) TestAnteHandle() { baseDenomGas: true, expectedNumOfTrades: sdk.ZeroInt(), expectedProfits: []*sdk.Coin{}, - expectedPoolPoints: 12, + expectedPoolPoints: 0, }, expectPass: true, }, @@ -109,7 +167,7 @@ func (suite *KeeperTestSuite) TestAnteHandle() { Amount: sdk.NewInt(24848), }, }, - expectedPoolPoints: 18, + expectedPoolPoints: 6, }, expectPass: true, }, @@ -125,7 +183,7 @@ func (suite *KeeperTestSuite) TestAnteHandle() { TokenOutDenom: "ibc/A0CC0CF735BFB30E730C70019D4218A1244FF383503FF7579C9201AB93CA9293", }, }, - TokenIn: sdk.NewCoin(types.AtomDenomination, sdk.NewInt(10000)), + TokenIn: sdk.NewCoin("Atom", sdk.NewInt(10000)), TokenOutMinAmount: sdk.NewInt(10000), }, }, @@ -137,7 +195,7 @@ func (suite *KeeperTestSuite) TestAnteHandle() { expectedNumOfTrades: sdk.NewInt(2), expectedProfits: []*sdk.Coin{ { - Denom: types.AtomDenomination, + Denom: "Atom", Amount: sdk.NewInt(5826), }, { @@ -145,7 +203,7 @@ func (suite *KeeperTestSuite) TestAnteHandle() { Amount: sdk.NewInt(24848), }, }, - expectedPoolPoints: 24, + expectedPoolPoints: 12, }, expectPass: true, }, @@ -173,7 +231,7 @@ func (suite *KeeperTestSuite) TestAnteHandle() { expectedNumOfTrades: sdk.NewInt(3), expectedProfits: []*sdk.Coin{ { - Denom: types.AtomDenomination, + Denom: "Atom", Amount: sdk.NewInt(5826), }, { @@ -181,6 +239,202 @@ func (suite *KeeperTestSuite) TestAnteHandle() { Amount: sdk.NewInt(56609900), }, }, + expectedPoolPoints: 21, + }, + expectPass: true, + }, + { + name: "Four Pool Arb Route - Hot Route Build", + params: param{ + msgs: []sdk.Msg{ + &poolmanagertypes.MsgSwapExactAmountIn{ + Sender: addr0.String(), + Routes: []poolmanagertypes.SwapAmountInRoute{ + { + PoolId: 37, + TokenOutDenom: "test/2", + }, + }, + TokenIn: sdk.NewCoin("Atom", sdk.NewInt(10000)), + TokenOutMinAmount: sdk.NewInt(100), + }, + }, + txFee: sdk.NewCoins(sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(10000))), + minGasPrices: sdk.NewDecCoins(), + gasLimit: 500000, + isCheckTx: false, + baseDenomGas: true, + expectedNumOfTrades: sdk.NewInt(4), + expectedProfits: []*sdk.Coin{ + { + Denom: "Atom", + Amount: sdk.NewInt(15_767_231), + }, + { + Denom: types.OsmosisDenomination, + Amount: sdk.NewInt(56_609_900), + }, + }, + expectedPoolPoints: 29, + }, + expectPass: true, + }, + { + name: "Two Pool Arb Route - Hot Route Build", + params: param{ + msgs: []sdk.Msg{ + &poolmanagertypes.MsgSwapExactAmountIn{ + Sender: addr0.String(), + Routes: []poolmanagertypes.SwapAmountInRoute{ + { + PoolId: 38, + TokenOutDenom: "test/3", + }, + }, + TokenIn: sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(10000)), + TokenOutMinAmount: sdk.NewInt(100), + }, + }, + txFee: sdk.NewCoins(sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(10000))), + minGasPrices: sdk.NewDecCoins(), + gasLimit: 500000, + isCheckTx: false, + baseDenomGas: true, + expectedNumOfTrades: sdk.NewInt(5), + expectedProfits: []*sdk.Coin{ + { + Denom: "Atom", + Amount: sdk.NewInt(15_767_231), + }, + { + Denom: "test/3", + Amount: sdk.NewInt(218_149_058), + }, + { + Denom: types.OsmosisDenomination, + Amount: sdk.NewInt(56_609_900), + }, + }, + expectedPoolPoints: 33, + }, + expectPass: true, + }, + { // This test the tx pool points limit caps the number of iterations + name: "Doomsday Test - Stableswap - Tx Pool Points Limit", + params: param{ + msgs: []sdk.Msg{ + &poolmanagertypes.MsgSwapExactAmountIn{ + Sender: addr0.String(), + Routes: []poolmanagertypes.SwapAmountInRoute{ + { + PoolId: 41, + TokenOutDenom: "usdc", + }, + }, + TokenIn: sdk.NewCoin("busd", sdk.NewInt(10000)), + TokenOutMinAmount: sdk.NewInt(100), + }, + }, + txFee: sdk.NewCoins(sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(10000))), + minGasPrices: sdk.NewDecCoins(), + gasLimit: 500000, + isCheckTx: false, + baseDenomGas: true, + expectedNumOfTrades: sdk.NewInt(5), + expectedProfits: []*sdk.Coin{ + { + Denom: "Atom", + Amount: sdk.NewInt(15_767_231), + }, + { + Denom: "test/3", + Amount: sdk.NewInt(218_149_058), + }, + { + Denom: types.OsmosisDenomination, + Amount: sdk.NewInt(56_609_900), + }, + }, + expectedPoolPoints: 33, + }, + expectPass: true, + }, + { // This test the block pool points limit caps the number of iterations within a tx + name: "Doomsday Test - Stableswap - Block Pool Points Limit - Within a tx", + params: param{ + msgs: []sdk.Msg{ + &poolmanagertypes.MsgSwapExactAmountIn{ + Sender: addr0.String(), + Routes: []poolmanagertypes.SwapAmountInRoute{ + { + PoolId: 41, + TokenOutDenom: "usdc", + }, + }, + TokenIn: sdk.NewCoin("busd", sdk.NewInt(10000)), + TokenOutMinAmount: sdk.NewInt(100), + }, + }, + txFee: sdk.NewCoins(sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(10000))), + minGasPrices: sdk.NewDecCoins(), + gasLimit: 500000, + isCheckTx: false, + baseDenomGas: true, + expectedNumOfTrades: sdk.NewInt(5), + expectedProfits: []*sdk.Coin{ + { + Denom: "Atom", + Amount: sdk.NewInt(15_767_231), + }, + { + Denom: "test/3", + Amount: sdk.NewInt(218_149_058), + }, + { + Denom: types.OsmosisDenomination, + Amount: sdk.NewInt(56_609_900), + }, + }, + expectedPoolPoints: 33, + }, + expectPass: true, + }, + { // This test the block pool points limit caps the number of txs processed if already reached the limit + name: "Doomsday Test - Stableswap - Block Pool Points Limit Already Reached - New tx", + params: param{ + msgs: []sdk.Msg{ + &poolmanagertypes.MsgSwapExactAmountIn{ + Sender: addr0.String(), + Routes: []poolmanagertypes.SwapAmountInRoute{ + { + PoolId: 41, + TokenOutDenom: "usdc", + }, + }, + TokenIn: sdk.NewCoin("busd", sdk.NewInt(10000)), + TokenOutMinAmount: sdk.NewInt(100), + }, + }, + txFee: sdk.NewCoins(sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(10000))), + minGasPrices: sdk.NewDecCoins(), + gasLimit: 500000, + isCheckTx: false, + baseDenomGas: true, + expectedNumOfTrades: sdk.NewInt(5), + expectedProfits: []*sdk.Coin{ + { + Denom: "Atom", + Amount: sdk.NewInt(15_767_231), + }, + { + Denom: "test/3", + Amount: sdk.NewInt(218_149_058), + }, + { + Denom: types.OsmosisDenomination, + Amount: sdk.NewInt(56_609_900), + }, + }, expectedPoolPoints: 33, }, expectPass: true, @@ -188,7 +442,8 @@ func (suite *KeeperTestSuite) TestAnteHandle() { } // Ensure that the max points per tx is enough for the test suite - suite.App.ProtoRevKeeper.SetMaxPointsPerTx(suite.Ctx, 40) + suite.App.ProtoRevKeeper.SetMaxPointsPerTx(suite.Ctx, 18) + suite.App.ProtoRevKeeper.SetMaxPointsPerBlock(suite.Ctx, 100) suite.App.ProtoRevKeeper.SetPoolWeights(suite.Ctx, types.PoolWeights{StableWeight: 5, BalancerWeight: 2, ConcentratedWeight: 2}) for _, tc := range tests { @@ -196,7 +451,6 @@ func (suite *KeeperTestSuite) TestAnteHandle() { suite.Ctx = suite.Ctx.WithIsCheckTx(tc.params.isCheckTx) suite.Ctx = suite.Ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) suite.Ctx = suite.Ctx.WithMinGasPrices(tc.params.minGasPrices) - msgs := tc.params.msgs privs, accNums, accSeqs := []cryptotypes.PrivKey{priv0}, []uint64{0}, []uint64{0} signerData := authsigning.SignerData{ @@ -214,7 +468,35 @@ func (suite *KeeperTestSuite) TestAnteHandle() { accSeqs[0], ) simapp.FundAccount(suite.App.BankKeeper, suite.Ctx, addr0, tc.params.txFee) - tx := suite.BuildTx(txBuilder, msgs, sigV2, "", tc.params.txFee, gasLimit) + + var tx authsigning.Tx + var msgs []sdk.Msg + + // Lower the max points per tx and block if the test cases are doomsday testing + if strings.Contains(tc.name, "Tx Pool Points Limit") { + suite.App.ProtoRevKeeper.SetMaxPointsPerTx(suite.Ctx, 5) + } else if strings.Contains(tc.name, "Block Pool Points Limit - Within a tx") { + suite.App.ProtoRevKeeper.SetMaxPointsPerBlock(suite.Ctx, 35) + } else if strings.Contains(tc.name, "Block Pool Points Limit Already Reached") { + suite.App.ProtoRevKeeper.SetMaxPointsPerBlock(suite.Ctx, 33) + } + + if strings.Contains(tc.name, "Doomsday") { + for i := 0; i < 100; i++ { + msgs = append(msgs, tc.params.msgs...) + } + + txBuilder.SetMsgs(msgs...) + txBuilder.SetSignatures(sigV2) + txBuilder.SetMemo("") + txBuilder.SetFeeAmount(tc.params.txFee) + txBuilder.SetGasLimit(gasLimit) + tx = txBuilder.GetTx() + } else { + msgs = tc.params.msgs + tx = suite.BuildTx(txBuilder, msgs, sigV2, "", tc.params.txFee, gasLimit) + } + protoRevDecorator := keeper.NewProtoRevDecorator(*suite.App.ProtoRevKeeper) posthandlerProtoRev := sdk.ChainAnteDecorators(protoRevDecorator) @@ -255,6 +537,14 @@ func (suite *KeeperTestSuite) TestAnteHandle() { } else { suite.Require().Error(err) } + + // Reset the max points per tx and block + if strings.Contains(tc.name, "Tx Pool Points Limit") { + suite.App.ProtoRevKeeper.SetMaxPointsPerTx(suite.Ctx, 18) + } else if strings.Contains(tc.name, "Block Pool Points Limit") { + suite.App.ProtoRevKeeper.SetMaxPointsPerBlock(suite.Ctx, 100) + } + }) } } @@ -361,6 +651,256 @@ func (suite *KeeperTestSuite) TestExtractSwappedPools() { }, expectPass: true, }, + { + name: "Single Swap Amount Out Test", + params: param{ + msgs: []sdk.Msg{ + &poolmanagertypes.MsgSwapExactAmountOut{ + Sender: addr0.String(), + Routes: []poolmanagertypes.SwapAmountOutRoute{ + { + PoolId: 28, + TokenInDenom: "ibc/BE1BB42D4BE3C30D50B68D7C41DB4DFCE9678E8EF8C539F6E6A9345048894FCC", + }, + }, + TokenOut: sdk.NewCoin("ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858", sdk.NewInt(10000)), + TokenInMaxAmount: sdk.NewInt(10000), + }, + }, + txFee: sdk.NewCoins(sdk.NewCoin("uosmo", sdk.NewInt(10000))), + minGasPrices: sdk.NewDecCoins(), + gasLimit: 500000, + isCheckTx: false, + baseDenomGas: true, + expectedNumOfPools: 1, + expectedSwappedPools: []keeper.SwapToBackrun{ + { + PoolId: 28, + TokenOutDenom: "ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858", + TokenInDenom: "ibc/BE1BB42D4BE3C30D50B68D7C41DB4DFCE9678E8EF8C539F6E6A9345048894FCC", + }, + }, + }, + expectPass: true, + }, + { + name: "Single Swap with multiple hops (swapOut)", + params: param{ + msgs: []sdk.Msg{ + &poolmanagertypes.MsgSwapExactAmountOut{ + Sender: addr0.String(), + Routes: []poolmanagertypes.SwapAmountOutRoute{ + { + PoolId: 28, + TokenInDenom: "atom", + }, + { + PoolId: 30, + TokenInDenom: "weth", + }, + { + PoolId: 35, + TokenInDenom: "bitcoin", + }, + }, + TokenOut: sdk.NewCoin("akash", sdk.NewInt(10000)), + TokenInMaxAmount: sdk.NewInt(10000), + }, + }, + txFee: sdk.NewCoins(sdk.NewCoin("uosmo", sdk.NewInt(10000))), + minGasPrices: sdk.NewDecCoins(), + gasLimit: 500000, + isCheckTx: false, + baseDenomGas: true, + expectedNumOfPools: 3, + expectedSwappedPools: []keeper.SwapToBackrun{ + { + PoolId: 35, + TokenOutDenom: "akash", + TokenInDenom: "bitcoin", + }, + { + PoolId: 30, + TokenOutDenom: "bitcoin", + TokenInDenom: "weth", + }, + { + PoolId: 28, + TokenOutDenom: "weth", + TokenInDenom: "atom", + }, + }, + }, + expectPass: true, + }, + { + name: "Single Swap with multiple hops (swapIn)", + params: param{ + msgs: []sdk.Msg{ + &poolmanagertypes.MsgSwapExactAmountIn{ + Sender: addr0.String(), + Routes: []poolmanagertypes.SwapAmountInRoute{ + { + PoolId: 28, + TokenOutDenom: "atom", + }, + { + PoolId: 30, + TokenOutDenom: "weth", + }, + { + PoolId: 35, + TokenOutDenom: "bitcoin", + }, + { + PoolId: 36, + TokenOutDenom: "juno", + }, + }, + TokenIn: sdk.NewCoin("akash", sdk.NewInt(10000)), + TokenOutMinAmount: sdk.NewInt(1), + }, + }, + txFee: sdk.NewCoins(sdk.NewCoin("uosmo", sdk.NewInt(10000))), + minGasPrices: sdk.NewDecCoins(), + gasLimit: 500000, + isCheckTx: false, + baseDenomGas: true, + expectedNumOfPools: 4, + expectedSwappedPools: []keeper.SwapToBackrun{ + { + PoolId: 28, + TokenOutDenom: "atom", + TokenInDenom: "akash", + }, + { + PoolId: 30, + TokenOutDenom: "weth", + TokenInDenom: "atom", + }, + { + PoolId: 35, + TokenOutDenom: "bitcoin", + TokenInDenom: "weth", + }, + { + PoolId: 36, + TokenOutDenom: "juno", + TokenInDenom: "bitcoin", + }, + }, + }, + expectPass: true, + }, + { + name: "Single Swap with multiple hops (gamm msg swapOut)", + params: param{ + msgs: []sdk.Msg{ + &gammtypes.MsgSwapExactAmountOut{ + Sender: addr0.String(), + Routes: []poolmanagertypes.SwapAmountOutRoute{ + { + PoolId: 28, + TokenInDenom: "atom", + }, + { + PoolId: 30, + TokenInDenom: "weth", + }, + { + PoolId: 35, + TokenInDenom: "bitcoin", + }, + }, + TokenOut: sdk.NewCoin("akash", sdk.NewInt(10000)), + TokenInMaxAmount: sdk.NewInt(10000), + }, + }, + txFee: sdk.NewCoins(sdk.NewCoin("uosmo", sdk.NewInt(10000))), + minGasPrices: sdk.NewDecCoins(), + gasLimit: 500000, + isCheckTx: false, + baseDenomGas: true, + expectedNumOfPools: 3, + expectedSwappedPools: []keeper.SwapToBackrun{ + { + PoolId: 35, + TokenOutDenom: "akash", + TokenInDenom: "bitcoin", + }, + { + PoolId: 30, + TokenOutDenom: "bitcoin", + TokenInDenom: "weth", + }, + { + PoolId: 28, + TokenOutDenom: "weth", + TokenInDenom: "atom", + }, + }, + }, + expectPass: true, + }, + { + name: "Single Swap with multiple hops (gamm swapIn)", + params: param{ + msgs: []sdk.Msg{ + &gammtypes.MsgSwapExactAmountIn{ + Sender: addr0.String(), + Routes: []poolmanagertypes.SwapAmountInRoute{ + { + PoolId: 28, + TokenOutDenom: "atom", + }, + { + PoolId: 30, + TokenOutDenom: "weth", + }, + { + PoolId: 35, + TokenOutDenom: "bitcoin", + }, + { + PoolId: 36, + TokenOutDenom: "juno", + }, + }, + TokenIn: sdk.NewCoin("akash", sdk.NewInt(10000)), + TokenOutMinAmount: sdk.NewInt(1), + }, + }, + txFee: sdk.NewCoins(sdk.NewCoin("uosmo", sdk.NewInt(10000))), + minGasPrices: sdk.NewDecCoins(), + gasLimit: 500000, + isCheckTx: false, + baseDenomGas: true, + expectedNumOfPools: 4, + expectedSwappedPools: []keeper.SwapToBackrun{ + { + PoolId: 28, + TokenOutDenom: "atom", + TokenInDenom: "akash", + }, + { + PoolId: 30, + TokenOutDenom: "weth", + TokenInDenom: "atom", + }, + { + PoolId: 35, + TokenOutDenom: "bitcoin", + TokenInDenom: "weth", + }, + { + PoolId: 36, + TokenOutDenom: "juno", + TokenInDenom: "bitcoin", + }, + }, + }, + expectPass: true, + }, } for _, tc := range tests { @@ -405,3 +945,73 @@ func (suite *KeeperTestSuite) TestExtractSwappedPools() { }) } } + +// benchmarkWrapper is a wrapper function for the benchmark tests. It sets up the suite, accepts the +// messages to be sent, and the expected number of trades. It then runs the benchmark and checks the +// number of trades after the post handler is run. +func benchmarkWrapper(b *testing.B, msgs []sdk.Msg, expectedTrades int) { + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + suite, tx, postHandler := setUpBenchmarkSuite(msgs) + + b.StartTimer() + postHandler(suite.Ctx, tx, false) + b.StopTimer() + + numberTrades, err := suite.App.ProtoRevKeeper.GetNumberOfTrades(suite.Ctx) + if err != nil { + if expectedTrades != 0 { + b.Fatal("error getting number of trades") + } + } + if !numberTrades.Equal(sdk.NewInt(int64(expectedTrades))) { + b.Fatalf("expected %d trades, got %d", expectedTrades, numberTrades) + } + } +} + +// setUpBenchmarkSuite sets up a app test suite, tx, and post handler for benchmark tests. +// It returns the app configured to the correct state, a valid tx, and the protorev post handler. +func setUpBenchmarkSuite(msgs []sdk.Msg) (*KeeperTestSuite, authsigning.Tx, sdk.AnteHandler) { + // Create a new test suite + suite := new(KeeperTestSuite) + suite.SetT(&testing.T{}) + suite.SetupTest() + + // Set up the app to the correct state to run the test + suite.Ctx = suite.Ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) + suite.App.ProtoRevKeeper.SetMaxPointsPerTx(suite.Ctx, 40) + suite.App.ProtoRevKeeper.SetPoolWeights(suite.Ctx, types.PoolWeights{StableWeight: 5, BalancerWeight: 2, ConcentratedWeight: 2}) + + // Init a new account and fund it with tokens for gas fees + priv0, _, addr0 := testdata.KeyTestPubAddr() + acc1 := suite.App.AccountKeeper.NewAccountWithAddress(suite.Ctx, addr0) + suite.App.AccountKeeper.SetAccount(suite.Ctx, acc1) + simapp.FundAccount(suite.App.BankKeeper, suite.Ctx, addr0, sdk.NewCoins(sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(10000)))) + + // Build the tx + privs, accNums, accSeqs := []cryptotypes.PrivKey{priv0}, []uint64{0}, []uint64{0} + signerData := authsigning.SignerData{ + ChainID: suite.Ctx.ChainID(), + AccountNumber: accNums[0], + Sequence: accSeqs[0], + } + txBuilder := suite.clientCtx.TxConfig.NewTxBuilder() + sigV2, _ := clienttx.SignWithPrivKey( + 1, + signerData, + txBuilder, + privs[0], + suite.clientCtx.TxConfig, + accSeqs[0], + ) + tx := suite.BuildTx(txBuilder, msgs, sigV2, "", sdk.NewCoins(sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(10000))), 500000) + + // Set up the post handler + protoRevDecorator := keeper.NewProtoRevDecorator(*suite.App.ProtoRevKeeper) + posthandlerProtoRev := sdk.ChainAnteDecorators(protoRevDecorator) + + return suite, tx, posthandlerProtoRev +} diff --git a/x/protorev/keeper/protorev.go b/x/protorev/keeper/protorev.go index f44ec5c3219..54aa982d799 100644 --- a/x/protorev/keeper/protorev.go +++ b/x/protorev/keeper/protorev.go @@ -280,9 +280,9 @@ func (k Keeper) GetPointCountForBlock(ctx sdk.Context) (uint64, error) { } // SetPointCountForBlock sets the number of pool points that have been consumed in the current block -func (k Keeper) SetPointCountForBlock(ctx sdk.Context, txCount uint64) { +func (k Keeper) SetPointCountForBlock(ctx sdk.Context, pointCount uint64) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefixPointCountForBlock) - store.Set(types.KeyPrefixPointCountForBlock, sdk.Uint64ToBigEndian(txCount)) + store.Set(types.KeyPrefixPointCountForBlock, sdk.Uint64ToBigEndian(pointCount)) } // IncrementPointCountForBlock increments the number of pool points that have been consumed in the current block diff --git a/x/protorev/keeper/protorev_test.go b/x/protorev/keeper/protorev_test.go index cb8f06b4b33..3d1af5850d2 100644 --- a/x/protorev/keeper/protorev_test.go +++ b/x/protorev/keeper/protorev_test.go @@ -60,9 +60,10 @@ func (suite *KeeperTestSuite) TestGetAllBaseDenoms() { // Should be initialized on genesis baseDenoms, err := suite.App.ProtoRevKeeper.GetAllBaseDenoms(suite.Ctx) suite.Require().NoError(err) - suite.Require().Equal(2, len(baseDenoms)) + suite.Require().Equal(3, len(baseDenoms)) suite.Require().Equal(baseDenoms[0].Denom, types.OsmosisDenomination) - suite.Require().Equal(baseDenoms[1].Denom, types.AtomDenomination) + suite.Require().Equal(baseDenoms[1].Denom, "Atom") + suite.Require().Equal(baseDenoms[2].Denom, "test/3") // Should be able to delete all base denoms suite.App.ProtoRevKeeper.DeleteBaseDenoms(suite.Ctx) @@ -83,31 +84,31 @@ func (suite *KeeperTestSuite) TestGetAllBaseDenoms() { // TestGetPoolForDenomPair tests the GetPoolForDenomPair, SetPoolForDenomPair, and DeleteAllPoolsForBaseDenom functions. func (suite *KeeperTestSuite) TestGetPoolForDenomPair() { // Should be able to set a pool for a denom pair - suite.App.ProtoRevKeeper.SetPoolForDenomPair(suite.Ctx, types.AtomDenomination, types.OsmosisDenomination, 1000) - pool, err := suite.App.ProtoRevKeeper.GetPoolForDenomPair(suite.Ctx, types.AtomDenomination, types.OsmosisDenomination) + suite.App.ProtoRevKeeper.SetPoolForDenomPair(suite.Ctx, "Atom", types.OsmosisDenomination, 1000) + pool, err := suite.App.ProtoRevKeeper.GetPoolForDenomPair(suite.Ctx, "Atom", types.OsmosisDenomination) suite.Require().NoError(err) suite.Require().Equal(uint64(1000), pool) // Should be able to add another pool for a denom pair - suite.App.ProtoRevKeeper.SetPoolForDenomPair(suite.Ctx, types.AtomDenomination, "weth", 2000) - pool, err = suite.App.ProtoRevKeeper.GetPoolForDenomPair(suite.Ctx, types.AtomDenomination, "weth") + suite.App.ProtoRevKeeper.SetPoolForDenomPair(suite.Ctx, "Atom", "weth", 2000) + pool, err = suite.App.ProtoRevKeeper.GetPoolForDenomPair(suite.Ctx, "Atom", "weth") suite.Require().NoError(err) suite.Require().Equal(uint64(2000), pool) - suite.App.ProtoRevKeeper.SetPoolForDenomPair(suite.Ctx, types.OsmosisDenomination, types.AtomDenomination, 3000) - pool, err = suite.App.ProtoRevKeeper.GetPoolForDenomPair(suite.Ctx, types.OsmosisDenomination, types.AtomDenomination) + suite.App.ProtoRevKeeper.SetPoolForDenomPair(suite.Ctx, types.OsmosisDenomination, "Atom", 3000) + pool, err = suite.App.ProtoRevKeeper.GetPoolForDenomPair(suite.Ctx, types.OsmosisDenomination, "Atom") suite.Require().NoError(err) suite.Require().Equal(uint64(3000), pool) // Should be able to delete all pools for a base denom - suite.App.ProtoRevKeeper.DeleteAllPoolsForBaseDenom(suite.Ctx, types.AtomDenomination) - pool, err = suite.App.ProtoRevKeeper.GetPoolForDenomPair(suite.Ctx, types.AtomDenomination, types.OsmosisDenomination) + suite.App.ProtoRevKeeper.DeleteAllPoolsForBaseDenom(suite.Ctx, "Atom") + pool, err = suite.App.ProtoRevKeeper.GetPoolForDenomPair(suite.Ctx, "Atom", types.OsmosisDenomination) suite.Require().Error(err) - pool, err = suite.App.ProtoRevKeeper.GetPoolForDenomPair(suite.Ctx, types.AtomDenomination, "weth") + pool, err = suite.App.ProtoRevKeeper.GetPoolForDenomPair(suite.Ctx, "Atom", "weth") suite.Require().Error(err) // Other denoms should still exist - pool, err = suite.App.ProtoRevKeeper.GetPoolForDenomPair(suite.Ctx, types.OsmosisDenomination, types.AtomDenomination) + pool, err = suite.App.ProtoRevKeeper.GetPoolForDenomPair(suite.Ctx, types.OsmosisDenomination, "Atom") suite.Require().NoError(err) suite.Require().Equal(uint64(3000), pool) } @@ -139,14 +140,14 @@ func (suite *KeeperTestSuite) TestGetDeveloperFees() { suite.Require().Equal(sdk.Coin{}, osmoFees) // Should be no atom fees on genesis - atomFees, err := suite.App.ProtoRevKeeper.GetDeveloperFees(suite.Ctx, types.AtomDenomination) + atomFees, err := suite.App.ProtoRevKeeper.GetDeveloperFees(suite.Ctx, "Atom") suite.Require().Error(err) suite.Require().Equal(sdk.Coin{}, atomFees) // Should be able to set the fees err = suite.App.ProtoRevKeeper.SetDeveloperFees(suite.Ctx, sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(100))) suite.Require().NoError(err) - err = suite.App.ProtoRevKeeper.SetDeveloperFees(suite.Ctx, sdk.NewCoin(types.AtomDenomination, sdk.NewInt(100))) + err = suite.App.ProtoRevKeeper.SetDeveloperFees(suite.Ctx, sdk.NewCoin("Atom", sdk.NewInt(100))) suite.Require().NoError(err) err = suite.App.ProtoRevKeeper.SetDeveloperFees(suite.Ctx, sdk.NewCoin("weth", sdk.NewInt(100))) @@ -154,9 +155,9 @@ func (suite *KeeperTestSuite) TestGetDeveloperFees() { osmoFees, err = suite.App.ProtoRevKeeper.GetDeveloperFees(suite.Ctx, types.OsmosisDenomination) suite.Require().NoError(err) suite.Require().Equal(sdk.NewCoin(types.OsmosisDenomination, sdk.NewInt(100)), osmoFees) - atomFees, err = suite.App.ProtoRevKeeper.GetDeveloperFees(suite.Ctx, types.AtomDenomination) + atomFees, err = suite.App.ProtoRevKeeper.GetDeveloperFees(suite.Ctx, "Atom") suite.Require().NoError(err) - suite.Require().Equal(sdk.NewCoin(types.AtomDenomination, sdk.NewInt(100)), atomFees) + suite.Require().Equal(sdk.NewCoin("Atom", sdk.NewInt(100)), atomFees) wethFees, err := suite.App.ProtoRevKeeper.GetDeveloperFees(suite.Ctx, "weth") suite.Require().NoError(err) suite.Require().Equal(sdk.NewCoin("weth", sdk.NewInt(100)), wethFees) diff --git a/x/protorev/keeper/rebalance.go b/x/protorev/keeper/rebalance.go index 5ce52b3eeff..90c706cf564 100644 --- a/x/protorev/keeper/rebalance.go +++ b/x/protorev/keeper/rebalance.go @@ -9,40 +9,48 @@ 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 []poolmanagertypes.SwapAmountInRoutes) (sdk.Coin, sdk.Int, poolmanagertypes.SwapAmountInRoutes) { +func (k Keeper) IterateRoutes(ctx sdk.Context, routes []RouteMetaData) (sdk.Coin, sdk.Int, poolmanagertypes.SwapAmountInRoutes) { var optimalRoute poolmanagertypes.SwapAmountInRoutes var maxProfitInputCoin sdk.Coin maxProfit := sdk.ZeroInt() - for _, route := range routes { - // Find the max profit for the route using the token out denom of the last pool in the route as the input token denom - inputDenom := route[len(route)-1].TokenOutDenom - inputCoin, profit, err := k.FindMaxProfitForRoute(ctx, route, inputDenom) - if err != nil { - k.Logger(ctx).Error("Error finding max profit for route: ", err) + // Get the total number of pool points that can be consumed in this transaction + remainingPoolPoints, err := k.RemainingPoolPointsForTx(ctx) + if err != nil { + return maxProfitInputCoin, maxProfit, optimalRoute + } + + // Iterate through the routes and find the optimal route for the given swap + for index := 0; index < len(routes) && remainingPoolPoints > 0; index++ { + // If the route consumes more pool points than we have remaining then we skip it + if routes[index].PoolPoints > remainingPoolPoints { continue } - // Filter out routes that don't have any profit - if profit.LTE(sdk.ZeroInt()) { + // Find the max profit for the route if it exists + inputCoin, profit, err := k.FindMaxProfitForRoute(ctx, routes[index], &remainingPoolPoints) + if err != nil { + k.Logger(ctx).Error("Error finding max profit for route: ", err) continue } - // If arb doesn't start and end with uosmo, then we convert the profit to uosmo, and compare profits in terms of uosmo - if inputCoin.Denom != types.OsmosisDenomination { - uosmoProfit, err := k.ConvertProfits(ctx, inputCoin, profit) - if err != nil { - k.Logger(ctx).Error("Error converting profits: ", err) - continue + // If the profit is greater than zero, then we convert the profits to uosmo and compare profits in terms of uosmo + if profit.GT(sdk.ZeroInt()) { + if inputCoin.Denom != types.OsmosisDenomination { + uosmoProfit, err := k.ConvertProfits(ctx, inputCoin, profit) + if err != nil { + k.Logger(ctx).Error("Error converting profits: ", err) + continue + } + profit = uosmoProfit } - profit = uosmoProfit - } - // Select the optimal route King of the Hill style (route with the highest profit will be executed) - if profit.GT(maxProfit) { - optimalRoute = route - maxProfit = profit - maxProfitInputCoin = inputCoin + // Select the optimal route King of the Hill style (route with the highest profit will be executed) + if profit.GT(maxProfit) { + optimalRoute = routes[index].Route + maxProfit = profit + maxProfitInputCoin = inputCoin + } } } @@ -94,28 +102,50 @@ 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 poolmanagertypes.SwapAmountInRoutes, inputDenom string) (sdk.Coin, sdk.Int, error) { +func (k Keeper) FindMaxProfitForRoute(ctx sdk.Context, route RouteMetaData, remainingPoolPoints *uint64) (sdk.Coin, sdk.Int, error) { + // Track the tokenIn amount/denom and the profit tokenIn := sdk.Coin{} profit := sdk.ZeroInt() + // Track the left and right bounds of the binary search curLeft := sdk.OneInt() curRight := types.MaxInputAmount - iteration := 0 - for curLeft.LT(curRight) && iteration < types.MaxIterations { - iteration++ + // Input denom used for cyclic arbitrage + inputDenom := route.Route[route.Route.Length()-1].TokenOutDenom + + // If a cyclic arb exists with an optimal amount in above our minimum amount in, + // then inputting the minimum amount in will result in a profit. So we check for that first. + // If there is no profit, then we can return early and not run the binary search. + _, minInProfit, err := k.EstimateMultihopProfit(ctx, inputDenom, curLeft.Mul(route.StepSize), route.Route) + if err != nil { + return sdk.Coin{}, sdk.ZeroInt(), err + } else if minInProfit.LTE(sdk.ZeroInt()) { + return sdk.Coin{}, sdk.ZeroInt(), nil + } + + // Increment the number of pool points consumed since we know this route will be profitable + *remainingPoolPoints -= route.PoolPoints + if err := k.IncrementPointCountForBlock(ctx, route.PoolPoints); err != nil { + return sdk.Coin{}, sdk.ZeroInt(), err + } + + // Extend the search range if the max input amount is too small + curLeft, curRight = k.ExtendSearchRangeIfNeeded(ctx, route, inputDenom, curLeft, curRight) + // Binary search to find the max profit + for iteration := 0; curLeft.LT(curRight) && iteration < types.MaxIterations; iteration++ { curMid := (curLeft.Add(curRight)).Quo(sdk.NewInt(2)) curMidPlusOne := curMid.Add(sdk.OneInt()) // Short circuit profit searching if there is an error in the GAMM module - _, profitMid, err := k.EstimateMultihopProfit(ctx, inputDenom, curMid.Mul(types.StepSize), route) + _, profitMid, err := k.EstimateMultihopProfit(ctx, inputDenom, curMid.Mul(route.StepSize), route.Route) if err != nil { return sdk.Coin{}, sdk.ZeroInt(), err } // Short circuit profit searching if there is an error in the GAMM module - tokenInMidPlusOne, profitMidPlusOne, err := k.EstimateMultihopProfit(ctx, inputDenom, curMidPlusOne.Mul(types.StepSize), route) + tokenInMidPlusOne, profitMidPlusOne, err := k.EstimateMultihopProfit(ctx, inputDenom, curMidPlusOne.Mul(route.StepSize), route.Route) if err != nil { return sdk.Coin{}, sdk.ZeroInt(), err } @@ -133,6 +163,32 @@ func (k Keeper) FindMaxProfitForRoute(ctx sdk.Context, route poolmanagertypes.Sw return tokenIn, profit, nil } +// Determine if the binary search range needs to be extended +func (k Keeper) ExtendSearchRangeIfNeeded(ctx sdk.Context, route RouteMetaData, inputDenom string, curLeft, curRight sdk.Int) (sdk.Int, sdk.Int) { + // Get the profit for the maximum amount in + _, maxInProfit, err := k.EstimateMultihopProfit(ctx, inputDenom, curRight.Mul(route.StepSize), route.Route) + if err != nil { + return curLeft, curRight + } + + // If the profit for the maximum amount in is still increasing, then we can increase the range of the binary search + if maxInProfit.GTE(sdk.ZeroInt()) { + // Get the profit for the maximum amount in + 1 + _, maxInProfitPlusOne, err := k.EstimateMultihopProfit(ctx, inputDenom, curRight.Add(sdk.OneInt()).Mul(route.StepSize), route.Route) + if err != nil { + return curLeft, curRight + } + + // Change the range of the binary search if the profit is still increasing + if maxInProfitPlusOne.GT(maxInProfit) { + curLeft = curRight + curRight = types.ExtendedMaxInputAmount + } + } + + return curLeft, curRight +} + // ExecuteTrade inputs a route, amount in, and rebalances the pool func (k Keeper) ExecuteTrade(ctx sdk.Context, route poolmanagertypes.SwapAmountInRoutes, inputCoin sdk.Coin) error { // Get the module address which will execute the trade @@ -169,3 +225,36 @@ func (k Keeper) ExecuteTrade(ctx sdk.Context, route poolmanagertypes.SwapAmountI return nil } + +// RemainingPoolPointsForTx calculates the number of pool points that can be consumed in the current transaction. +// Returns a pointer that will be used throughout the lifetime of a transaction. +func (k Keeper) RemainingPoolPointsForTx(ctx sdk.Context) (uint64, error) { + maxRoutesPerTx, err := k.GetMaxPointsPerTx(ctx) + if err != nil { + return 0, err + } + + maxRoutesPerBlock, err := k.GetMaxPointsPerBlock(ctx) + if err != nil { + return 0, err + } + + currentRouteCount, err := k.GetPointCountForBlock(ctx) + if err != nil { + return 0, err + } + + // Edge case where the number of routes 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 + } + + // Calculate the number of routes that can be iterated over + numberOfIterableRoutes := maxRoutesPerBlock - currentRouteCount + if numberOfIterableRoutes > maxRoutesPerTx { + return maxRoutesPerTx, nil + } + + return numberOfIterableRoutes, nil +} diff --git a/x/protorev/keeper/rebalance_test.go b/x/protorev/keeper/rebalance_test.go index cc2f205fdce..b35e5c002a1 100644 --- a/x/protorev/keeper/rebalance_test.go +++ b/x/protorev/keeper/rebalance_test.go @@ -4,6 +4,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" poolmanagertypes "github.com/osmosis-labs/osmosis/v14/x/poolmanager/types" + protorevtypes "github.com/osmosis-labs/osmosis/v14/x/protorev/keeper" "github.com/osmosis-labs/osmosis/v14/x/protorev/types" ) @@ -72,7 +73,7 @@ var routeDiffDenom = poolmanagertypes.SwapAmountInRoutes{ }, poolmanagertypes.SwapAmountInRoute{ PoolId: 33, - TokenOutDenom: types.AtomDenomination, + TokenOutDenom: "Atom", }} // No Arbitrage Opportunity @@ -101,7 +102,7 @@ var routeStableSwap = poolmanagertypes.SwapAmountInRoutes{ TokenOutDenom: "usdc", }, poolmanagertypes.SwapAmountInRoute{ - PoolId: 34, + PoolId: 40, TokenOutDenom: "busd", }, poolmanagertypes.SwapAmountInRoute{ @@ -109,12 +110,73 @@ var routeStableSwap = poolmanagertypes.SwapAmountInRoutes{ TokenOutDenom: "uosmo", }} +// Four Pool Test Route (Mainnet Block: 1855422) +// expectedAmtIn: sdk.NewInt(1_147_000_000) +// expectedProfit: sdk.NewInt(15_761_405) +var fourPoolRoute = poolmanagertypes.SwapAmountInRoutes{ + poolmanagertypes.SwapAmountInRoute{ + PoolId: 34, + TokenOutDenom: "test/1", + }, + poolmanagertypes.SwapAmountInRoute{ + PoolId: 35, + TokenOutDenom: types.OsmosisDenomination, + }, + poolmanagertypes.SwapAmountInRoute{ + PoolId: 36, + TokenOutDenom: "test/2", + }, + poolmanagertypes.SwapAmountInRoute{ + PoolId: 37, + TokenOutDenom: "Atom", + }, +} + +// Two Pool Test Route (Mainnet Block: 6_300_675) +// expectedAmtIn: sdk.NewInt(989_000_000) +// expectedProfit: sdk.NewInt(218_149_058) +var twoPoolRoute = poolmanagertypes.SwapAmountInRoutes{ + poolmanagertypes.SwapAmountInRoute{ + PoolId: 38, + TokenOutDenom: types.OsmosisDenomination, + }, + poolmanagertypes.SwapAmountInRoute{ + PoolId: 39, + TokenOutDenom: "test/3", + }, +} + +// Tests the binary search range extends to the correct amount +var extendedRangeRoute = poolmanagertypes.SwapAmountInRoutes{ + poolmanagertypes.SwapAmountInRoute{ + PoolId: 42, + TokenOutDenom: "usdy", + }, + poolmanagertypes.SwapAmountInRoute{ + PoolId: 43, + TokenOutDenom: "usdx", + }, +} + +// EstimateMultiHopSwap Panic catching test +var panicRoute = poolmanagertypes.SwapAmountInRoutes{ + poolmanagertypes.SwapAmountInRoute{ + PoolId: 44, + TokenOutDenom: "usdy", + }, + poolmanagertypes.SwapAmountInRoute{ + PoolId: 45, + TokenOutDenom: "usdx", + }, +} + func (suite *KeeperTestSuite) TestFindMaxProfitRoute() { type param struct { - route poolmanagertypes.SwapAmountInRoutes - expectedAmtIn sdk.Int - expectedProfit sdk.Int + route poolmanagertypes.SwapAmountInRoutes + expectedAmtIn sdk.Int + expectedProfit sdk.Int + routePoolPoints uint64 } tests := []struct { @@ -122,51 +184,122 @@ func (suite *KeeperTestSuite) TestFindMaxProfitRoute() { param param expectPass bool }{ - {name: "Mainnet Arb Route - 2 Asset, Same Weights (Block: 5905150)", + { + name: "Mainnet Arb Route - 2 Asset, Same Weights (Block: 5905150)", param: param{ - route: routeTwoAssetSameWeight, - expectedAmtIn: sdk.NewInt(10000000), - expectedProfit: sdk.NewInt(24848)}, - expectPass: true}, - {name: "Mainnet Arb Route - Multi Asset, Same Weights (Block: 6906570)", + route: routeTwoAssetSameWeight, + expectedAmtIn: sdk.NewInt(10000000), + expectedProfit: sdk.NewInt(24848), + routePoolPoints: 6, + }, + expectPass: true, + }, + { + name: "Mainnet Arb Route - Multi Asset, Same Weights (Block: 6906570)", + param: param{ + route: routeMultiAssetSameWeight, + expectedAmtIn: sdk.NewInt(5000000), + expectedProfit: sdk.NewInt(4538), + routePoolPoints: 6, + }, + expectPass: true, + }, + { + name: "Arb Route - Multi Asset, Same Weights - Pool 22 instead of 26 (Block: 6906570)", param: param{ - route: routeMultiAssetSameWeight, - expectedAmtIn: sdk.NewInt(5000000), - expectedProfit: sdk.NewInt(4538)}, - expectPass: true}, - {name: "Arb Route - Multi Asset, Same Weights - Pool 22 instead of 26 (Block: 6906570)", + route: routeMostProfitable, + expectedAmtIn: sdk.NewInt(520000000), + expectedProfit: sdk.NewInt(67511675), + routePoolPoints: 6, + }, + expectPass: true, + }, + { + name: "Mainnet Arb Route - Multi Asset, Different Weights (Block: 6908256)", param: param{ - route: routeMostProfitable, - expectedAmtIn: sdk.NewInt(520000000), - expectedProfit: sdk.NewInt(67511675)}, - expectPass: true}, - {name: "Mainnet Arb Route - Multi Asset, Different Weights (Block: 6908256)", + route: routeDiffDenom, + expectedAmtIn: sdk.NewInt(4000000), + expectedProfit: sdk.NewInt(5826), + routePoolPoints: 6, + }, + expectPass: true, + }, + { + name: "StableSwap Test Route", param: param{ - route: routeDiffDenom, - expectedAmtIn: sdk.NewInt(4000000), - expectedProfit: sdk.NewInt(5826)}, - expectPass: true}, - {name: "StableSwap Test Route", + route: routeStableSwap, + expectedAmtIn: sdk.NewInt(138000000), + expectedProfit: sdk.NewInt(56585052), + routePoolPoints: 9, + }, + expectPass: true, + }, + { + name: "No Arbitrage Opportunity", param: param{ - route: routeStableSwap, - expectedAmtIn: sdk.NewInt(138000000), - expectedProfit: sdk.NewInt(56585052)}, - expectPass: true}, - {name: "No Arbitrage Opportunity", + route: routeNoArb, + expectedAmtIn: sdk.Int{}, + expectedProfit: sdk.NewInt(0), + routePoolPoints: 0, + }, + expectPass: true, + }, + { + name: "Four Pool Test Route", param: param{ - route: routeNoArb, - expectedAmtIn: sdk.Int{}, - expectedProfit: sdk.NewInt(0)}, - expectPass: true}, + route: fourPoolRoute, + expectedAmtIn: sdk.NewInt(1_147_000_000), + expectedProfit: sdk.NewInt(15_761_405), + routePoolPoints: 8, + }, + expectPass: true, + }, + { + name: "Two Pool Test Route", + param: param{ + route: twoPoolRoute, + expectedAmtIn: sdk.NewInt(989_000_000), + expectedProfit: sdk.NewInt(218_149_058), + routePoolPoints: 4, + }, + expectPass: true, + }, + { + name: "Extended Range Test Route", + param: param{ + route: extendedRangeRoute, + expectedAmtIn: sdk.NewInt(131_072_000_000), + expectedProfit: sdk.NewInt(20_900_656_975), + routePoolPoints: 10, + }, + expectPass: true, + }, + { + name: "Panic Route", + param: param{ + route: panicRoute, + expectedAmtIn: sdk.NewInt(0), + expectedProfit: sdk.NewInt(0), + routePoolPoints: 0, + }, + expectPass: false, + }, } for _, test := range tests { suite.Run(test.name, func() { + // init the route + remainingPoolPoints := uint64(1000) + route := protorevtypes.RouteMetaData{ + Route: test.param.route, + PoolPoints: test.param.routePoolPoints, + StepSize: sdk.NewInt(1_000_000), + } amtIn, profit, err := suite.App.ProtoRevKeeper.FindMaxProfitForRoute( suite.Ctx, - test.param.route, - test.param.route[2].TokenOutDenom, + route, + &remainingPoolPoints, ) if test.expectPass { @@ -176,6 +309,9 @@ func (suite *KeeperTestSuite) TestFindMaxProfitRoute() { } else { suite.Require().Error(err) } + + // check that the remaining pool points is correct + suite.Require().Equal(uint64(1000), remainingPoolPoints+test.param.routePoolPoints) }) } } @@ -189,10 +325,11 @@ func (suite *KeeperTestSuite) TestExecuteTrade() { } tests := []struct { - name string - param param - arbDenom string - expectPass bool + name string + param param + arbDenom string + expectPass bool + expectedNumOfTrades sdk.Int }{ { name: "Mainnet Arb Route", @@ -201,8 +338,9 @@ func (suite *KeeperTestSuite) TestExecuteTrade() { inputCoin: sdk.NewCoin("uosmo", sdk.NewInt(10100000)), expectedProfit: sdk.NewInt(24852), }, - arbDenom: types.OsmosisDenomination, - expectPass: true, + arbDenom: types.OsmosisDenomination, + expectPass: true, + expectedNumOfTrades: sdk.NewInt(1), }, { name: "No arbitrage opportunity - expect error at multihopswap due to profitability invariant", @@ -224,6 +362,28 @@ func (suite *KeeperTestSuite) TestExecuteTrade() { arbDenom: types.OsmosisDenomination, expectPass: false, }, + { + name: "4-Pool Route Arb", + param: param{ + route: fourPoolRoute, + inputCoin: sdk.NewCoin("Atom", sdk.NewInt(1_147_000_000)), + expectedProfit: sdk.NewInt(15_761_405), + }, + arbDenom: "Atom", + expectPass: true, + expectedNumOfTrades: sdk.NewInt(2), + }, + { + name: "2-Pool Route Arb", + param: param{ + route: twoPoolRoute, + inputCoin: sdk.NewCoin("test/3", sdk.NewInt(989_000_000)), + expectedProfit: sdk.NewInt(218_149_058), + }, + arbDenom: "test/3", + expectPass: true, + expectedNumOfTrades: sdk.NewInt(3), + }, } for _, test := range tests { @@ -252,7 +412,7 @@ func (suite *KeeperTestSuite) TestExecuteTrade() { totalNumberOfTrades, err := suite.App.ProtoRevKeeper.GetNumberOfTrades(suite.Ctx) suite.Require().NoError(err) - suite.Require().Equal(sdk.OneInt(), totalNumberOfTrades) + suite.Require().Equal(test.expectedNumOfTrades, totalNumberOfTrades) } else { suite.Require().Error(err) } @@ -308,22 +468,52 @@ func (suite *KeeperTestSuite) TestIterateRoutes() { params: paramm{ routes: []poolmanagertypes.SwapAmountInRoutes{routeNoArb, routeDiffDenom}, expectedMaxProfitAmount: sdk.NewInt(4880), - expectedMaxProfitInputCoin: sdk.NewCoin(types.AtomDenomination, sdk.NewInt(4000000)), + expectedMaxProfitInputCoin: sdk.NewCoin("Atom", sdk.NewInt(4000000)), expectedOptimalRoute: routeDiffDenom, - arbDenom: types.AtomDenomination, + arbDenom: "Atom", + }, + expectPass: true, + }, + {name: "Four-pool route test", + params: paramm{ + routes: []poolmanagertypes.SwapAmountInRoutes{fourPoolRoute}, + expectedMaxProfitAmount: sdk.NewInt(13_202_729), + expectedMaxProfitInputCoin: sdk.NewCoin("Atom", sdk.NewInt(1_147_000_000)), + expectedOptimalRoute: fourPoolRoute, + arbDenom: "Atom", + }, + expectPass: true, + }, + {name: "Two-pool route test", + params: paramm{ + routes: []poolmanagertypes.SwapAmountInRoutes{twoPoolRoute}, + expectedMaxProfitAmount: sdk.NewInt(198_653_535), + expectedMaxProfitInputCoin: sdk.NewCoin("test/3", sdk.NewInt(989_000_000)), + expectedOptimalRoute: twoPoolRoute, + arbDenom: "test/3", }, expectPass: true, }, } for _, test := range tests { - maxProfitInputCoin, maxProfitAmount, optimalRoute := suite.App.ProtoRevKeeper.IterateRoutes(suite.Ctx, test.params.routes) + suite.Run(test.name, func() { + routes := make([]protorevtypes.RouteMetaData, len(test.params.routes)) + for i, route := range test.params.routes { + routes[i] = protorevtypes.RouteMetaData{ + Route: route, + PoolPoints: 0, + StepSize: sdk.NewInt(1_000_000), + } + } - if test.expectPass { - suite.Require().Equal(test.params.expectedMaxProfitAmount, maxProfitAmount) - suite.Require().Equal(test.params.expectedMaxProfitInputCoin, maxProfitInputCoin) - suite.Require().Equal(test.params.expectedOptimalRoute, optimalRoute) - } + maxProfitInputCoin, maxProfitAmount, optimalRoute := suite.App.ProtoRevKeeper.IterateRoutes(suite.Ctx, routes) + if test.expectPass { + suite.Require().Equal(test.params.expectedMaxProfitAmount, maxProfitAmount) + suite.Require().Equal(test.params.expectedMaxProfitInputCoin, maxProfitInputCoin) + suite.Require().Equal(test.params.expectedOptimalRoute, optimalRoute) + } + }) } } @@ -342,7 +532,7 @@ func (suite *KeeperTestSuite) TestConvertProfits() { }{ {name: "Convert atom to uosmo", param: param{ - inputCoin: sdk.NewCoin(types.AtomDenomination, sdk.NewInt(100)), + inputCoin: sdk.NewCoin("Atom", sdk.NewInt(100)), profit: sdk.NewInt(10), expectedUosmoProfit: sdk.NewInt(8), }, @@ -377,3 +567,64 @@ func (suite *KeeperTestSuite) TestConvertProfits() { } } } + +// TestRemainingPoolPointsForTx tests the RemainingPoolPointsForTx function. +func (suite *KeeperTestSuite) TestRemainingPoolPointsForTx() { + cases := []struct { + description string + maxRoutesPerTx uint64 + maxRoutesPerBlock uint64 + currentRouteCount uint64 + expectedPointCount uint64 + }{ + { + description: "Max pool points per tx is 10 and max pool points per block is 100", + maxRoutesPerTx: 10, + maxRoutesPerBlock: 100, + currentRouteCount: 0, + expectedPointCount: 10, + }, + { + description: "Max pool points per tx is 10, max pool points per block is 100, and current point count is 90", + maxRoutesPerTx: 10, + maxRoutesPerBlock: 100, + currentRouteCount: 90, + expectedPointCount: 10, + }, + { + description: "Max pool points per tx is 10, max pool points per block is 100, and current point count is 100", + maxRoutesPerTx: 10, + maxRoutesPerBlock: 100, + currentRouteCount: 100, + expectedPointCount: 0, + }, + { + description: "Max pool points per tx is 10, max pool points per block is 100, and current point count is 95", + maxRoutesPerTx: 10, + maxRoutesPerBlock: 100, + currentRouteCount: 95, + expectedPointCount: 5, + }, + { + description: "Checking overflow", + maxRoutesPerTx: 10, + maxRoutesPerBlock: 100, + currentRouteCount: 105, + expectedPointCount: 0, + }, + } + + for _, tc := range cases { + suite.Run(tc.description, func() { + suite.SetupTest() + + suite.App.ProtoRevKeeper.SetMaxPointsPerTx(suite.Ctx, tc.maxRoutesPerTx) + suite.App.ProtoRevKeeper.SetMaxPointsPerBlock(suite.Ctx, tc.maxRoutesPerBlock) + suite.App.ProtoRevKeeper.SetPointCountForBlock(suite.Ctx, tc.currentRouteCount) + + points, err := suite.App.ProtoRevKeeper.RemainingPoolPointsForTx(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 eb384459799..b0e15a9a593 100644 --- a/x/protorev/keeper/routes.go +++ b/x/protorev/keeper/routes.go @@ -9,17 +9,26 @@ import ( "github.com/osmosis-labs/osmosis/v14/x/protorev/types" ) +type RouteMetaData struct { + // The route that was built + Route poolmanagertypes.SwapAmountInRoutes + // The number of pool points that were consumed to build the route + PoolPoints uint64 + // The step size that should be used in the binary search for the optimal swap amount + StepSize sdk.Int +} + // BuildRoutes builds all of the possible arbitrage routes given the tokenIn, tokenOut and poolId that were used in the swap. -func (k Keeper) BuildRoutes(ctx sdk.Context, tokenIn, tokenOut string, poolId uint64, remainingPoolPoints *uint64) []poolmanagertypes.SwapAmountInRoutes { - routes := make([]poolmanagertypes.SwapAmountInRoutes, 0) +func (k Keeper) BuildRoutes(ctx sdk.Context, tokenIn, tokenOut string, poolId uint64) []RouteMetaData { + routes := make([]RouteMetaData, 0) // Append hot routes if they exist - if tokenPairRoutes, err := k.BuildHotRoutes(ctx, tokenIn, tokenOut, poolId, remainingPoolPoints); err == nil { + if tokenPairRoutes, err := k.BuildHotRoutes(ctx, tokenIn, tokenOut, poolId); err == nil { routes = append(routes, tokenPairRoutes...) } // Append highest liquidity routes if they exist - if highestLiquidityRoutes, err := k.BuildHighestLiquidityRoutes(ctx, tokenIn, tokenOut, poolId, remainingPoolPoints); err == nil { + if highestLiquidityRoutes, err := k.BuildHighestLiquidityRoutes(ctx, tokenIn, tokenOut, poolId); err == nil { routes = append(routes, highestLiquidityRoutes...) } @@ -27,21 +36,18 @@ func (k Keeper) BuildRoutes(ctx sdk.Context, tokenIn, tokenOut string, poolId ui } // BuildHotRoutes builds all of the possible arbitrage routes using the hot routes method. -func (k Keeper) BuildHotRoutes(ctx sdk.Context, tokenIn, tokenOut string, poolId uint64, remainingPoolPoints *uint64) ([]poolmanagertypes.SwapAmountInRoutes, error) { - if *remainingPoolPoints <= 0 { - return []poolmanagertypes.SwapAmountInRoutes{}, fmt.Errorf("the number of pool points that can be consumed has been reached") - } - +func (k Keeper) BuildHotRoutes(ctx sdk.Context, tokenIn, tokenOut string, poolId uint64) ([]RouteMetaData, error) { + routes := make([]RouteMetaData, 0) // Get all of the routes from the store that match the given tokenIn and tokenOut tokenPairArbRoutes, err := k.GetTokenPairArbRoutes(ctx, tokenIn, tokenOut) if err != nil { - return []poolmanagertypes.SwapAmountInRoutes{}, err + return routes, err } // Iterate through all of the routes and build hot routes - routes := make([]poolmanagertypes.SwapAmountInRoutes, 0) - for index := 0; index < len(tokenPairArbRoutes.ArbRoutes) && *remainingPoolPoints > 0; index++ { - if newRoute, err := k.BuildHotRoute(ctx, tokenPairArbRoutes.ArbRoutes[index], poolId, remainingPoolPoints); err == nil { + for _, route := range tokenPairArbRoutes.ArbRoutes { + if newRoute, err := k.BuildHotRoute(ctx, route, poolId); err == nil { + newRoute.StepSize = *tokenPairArbRoutes.StepSize routes = append(routes, newRoute) } } @@ -50,7 +56,7 @@ func (k Keeper) BuildHotRoutes(ctx sdk.Context, tokenIn, tokenOut string, poolId } // BuildHotRoute constructs a cyclic arbitrage route given a hot route and swap that should be placed in the hot route. -func (k Keeper) BuildHotRoute(ctx sdk.Context, route *types.Route, poolId uint64, remainingPoolPoints *uint64) (poolmanagertypes.SwapAmountInRoutes, error) { +func (k Keeper) BuildHotRoute(ctx sdk.Context, route *types.Route, poolId uint64) (RouteMetaData, error) { newRoute := make(poolmanagertypes.SwapAmountInRoutes, 0) for _, trade := range route.Trades { @@ -68,26 +74,32 @@ func (k Keeper) BuildHotRoute(ctx sdk.Context, route *types.Route, poolId uint64 } } - // Check that the route is valid and update the number of pool points that can be consumed after the route is built - if err := k.CheckAndUpdateRouteState(ctx, newRoute, remainingPoolPoints); err != nil { - return poolmanagertypes.SwapAmountInRoutes{}, err + // Check that the route is valid and update the number of pool points that this route will consume when simulating and executing trades + routePoolPoints, err := k.CalculateRoutePoolPoints(ctx, newRoute) + if err != nil { + return RouteMetaData{}, err } - return newRoute, nil + return RouteMetaData{ + Route: newRoute, + PoolPoints: routePoolPoints, + }, nil } // BuildHighestLiquidityRoutes builds cyclic arbitrage routes using the highest liquidity method. The base denoms are sorted by priority // and routes are built in a greedy manner. -func (k Keeper) BuildHighestLiquidityRoutes(ctx sdk.Context, tokenIn, tokenOut string, poolId uint64, remainingPoolPoints *uint64) ([]poolmanagertypes.SwapAmountInRoutes, error) { - routes := make([]poolmanagertypes.SwapAmountInRoutes, 0) +func (k Keeper) BuildHighestLiquidityRoutes(ctx sdk.Context, tokenIn, tokenOut string, poolId uint64) ([]RouteMetaData, error) { + routes := make([]RouteMetaData, 0) baseDenoms, err := k.GetAllBaseDenoms(ctx) if err != nil { return routes, err } - // Iterate through all denoms greedily and build routes until the max number of pool points to be consumed is reached - for index := 0; index < len(baseDenoms) && *remainingPoolPoints > 0; index++ { - if newRoute, err := k.BuildHighestLiquidityRoute(ctx, baseDenoms[index].Denom, tokenIn, tokenOut, poolId, remainingPoolPoints); err == nil { + // Iterate through all denoms greedily. When simulating and executing trades, routes that are closer to the beginning of the list + // have priority over those that are later in the list. This way we can build routes that are more likely to succeed and bring in + // higher profits. + for _, baseDenom := range baseDenoms { + if newRoute, err := k.BuildHighestLiquidityRoute(ctx, baseDenom, tokenIn, tokenOut, poolId); err == nil { routes = append(routes, newRoute) } } @@ -96,11 +108,11 @@ func (k Keeper) BuildHighestLiquidityRoutes(ctx sdk.Context, tokenIn, tokenOut s } // BuildHighestLiquidityRoute constructs a cyclic arbitrage route that is starts/ends with swapDenom (ex. osmo) given the swap (tokenIn, tokenOut, poolId). -func (k Keeper) BuildHighestLiquidityRoute(ctx sdk.Context, swapDenom, tokenIn, tokenOut string, poolId uint64, remainingPoolPoints *uint64) (poolmanagertypes.SwapAmountInRoutes, error) { +func (k Keeper) BuildHighestLiquidityRoute(ctx sdk.Context, swapDenom *types.BaseDenom, tokenIn, tokenOut string, poolId uint64) (RouteMetaData, error) { // Create the first swap for the MultiHopSwap Route - entryPoolId, err := k.GetPoolForDenomPair(ctx, swapDenom, tokenOut) + entryPoolId, err := k.GetPoolForDenomPair(ctx, swapDenom.Denom, tokenOut) if err != nil { - return poolmanagertypes.SwapAmountInRoutes{}, err + return RouteMetaData{}, err } entryHop := poolmanagertypes.SwapAmountInRoute{ PoolId: entryPoolId, @@ -113,45 +125,46 @@ func (k Keeper) BuildHighestLiquidityRoute(ctx sdk.Context, swapDenom, tokenIn, } // Creating the third swap in the arb - exitPoolId, err := k.GetPoolForDenomPair(ctx, swapDenom, tokenIn) + exitPoolId, err := k.GetPoolForDenomPair(ctx, swapDenom.Denom, tokenIn) if err != nil { - return poolmanagertypes.SwapAmountInRoutes{}, err + return RouteMetaData{}, err } exitHop := poolmanagertypes.SwapAmountInRoute{ PoolId: exitPoolId, - TokenOutDenom: swapDenom, + TokenOutDenom: swapDenom.Denom, } newRoute := poolmanagertypes.SwapAmountInRoutes{entryHop, middleHop, exitHop} - // Check that the route is valid and update the number of pool points that can be consumed after the route is built - if err := k.CheckAndUpdateRouteState(ctx, newRoute, remainingPoolPoints); err != nil { - return poolmanagertypes.SwapAmountInRoutes{}, err + // Check that the route is valid and update the number of pool points that this route will consume when simulating and executing trades + routePoolPoints, err := k.CalculateRoutePoolPoints(ctx, newRoute) + if err != nil { + return RouteMetaData{}, err } - return newRoute, nil + return RouteMetaData{ + Route: newRoute, + PoolPoints: routePoolPoints, + StepSize: swapDenom.StepSize, + }, nil } -// CheckAndUpdateRouteState checks if the cyclic arbitrage route that was created via the highest liquidity route or hot route method is valid. -// If the route is too expensive to iterate through, has a inactive or invalid pool, or unsupported pool type, an error is returned. -func (k Keeper) CheckAndUpdateRouteState(ctx sdk.Context, route poolmanagertypes.SwapAmountInRoutes, remainingPoolPoints *uint64) error { - if *remainingPoolPoints <= 0 { - return fmt.Errorf("the number of routes that can be iterated through has been reached") - } - +// CalculateRoutePoolPoints calculates the number of pool points that will be consumed by a route when simulating and executing trades. This +// is only added to the global pool point counter if the route simulated is minimally profitable i.e. it will make a profit. +func (k Keeper) CalculateRoutePoolPoints(ctx sdk.Context, route poolmanagertypes.SwapAmountInRoutes) (uint64, error) { + // Calculate the number of pool points this route will consume poolWeights := k.GetPoolWeights(ctx) - totalWeight := uint64(0) poolIds := route.PoolIds() - for index := 0; totalWeight <= *remainingPoolPoints && index < len(poolIds); index++ { + for _, poolId := range poolIds { // Ensure that all of the pools in the route exist and are active - if err := k.IsValidPool(ctx, poolIds[index]); err != nil { - return err + if err := k.IsValidPool(ctx, poolId); err != nil { + return 0, err } - poolType, err := k.gammKeeper.GetPoolType(ctx, poolIds[index]) + poolType, err := k.gammKeeper.GetPoolType(ctx, poolId) if err != nil { - return err + return 0, err } switch poolType { @@ -163,20 +176,21 @@ func (k Keeper) CheckAndUpdateRouteState(ctx sdk.Context, route poolmanagertypes // case poolmanagertypes.Concentrated: // totalWeight += poolWeights.ConcentratedWeight default: - return fmt.Errorf("invalid pool type") + return 0, fmt.Errorf("invalid pool type") } } - // Check that the route can be iterated - if *remainingPoolPoints < totalWeight { - return fmt.Errorf("the total weight of the route is too expensive to iterate through: %d > %d", totalWeight, *remainingPoolPoints) + remainingPoolPoints, err := k.RemainingPoolPointsForTx(ctx) + if err != nil { + return 0, err } - if err := k.IncrementPointCountForBlock(ctx, totalWeight); err != nil { - return err + // If the route consumes more pool points than are available, return an error + if totalWeight > remainingPoolPoints { + return 0, fmt.Errorf("route consumes %d pool points but only %d are available", totalWeight, remainingPoolPoints) } - *remainingPoolPoints -= totalWeight - return nil + + return totalWeight, nil } // IsValidPool checks if the pool is active and exists @@ -190,30 +204,3 @@ func (k Keeper) IsValidPool(ctx sdk.Context, poolId uint64) error { } return nil } - -// RemainingPoolPointsForTx calculates the number of pool points that can be consumed in the current transaction. -// Returns a pointer that will be used throughout the lifetime of a transaction. -func (k Keeper) RemainingPoolPointsForTx(ctx sdk.Context) (*uint64, error) { - maxRoutesPerTx, err := k.GetMaxPointsPerTx(ctx) - if err != nil { - return nil, err - } - - maxRoutesPerBlock, err := k.GetMaxPointsPerBlock(ctx) - if err != nil { - return nil, err - } - - currentRouteCount, err := k.GetPointCountForBlock(ctx) - if err != nil { - return nil, err - } - - // Calculate the number of routes that can be iterated over - numberOfIterableRoutes := maxRoutesPerBlock - currentRouteCount - if numberOfIterableRoutes > maxRoutesPerTx { - numberOfIterableRoutes = maxRoutesPerTx - } - - return &numberOfIterableRoutes, nil -} diff --git a/x/protorev/keeper/routes_test.go b/x/protorev/keeper/routes_test.go index 7fe57be67c7..f108f657b2b 100644 --- a/x/protorev/keeper/routes_test.go +++ b/x/protorev/keeper/routes_test.go @@ -1,6 +1,8 @@ package keeper_test import ( + sdk "github.com/cosmos/cosmos-sdk/types" + poolmanagertypes "github.com/osmosis-labs/osmosis/v14/x/poolmanager/types" "github.com/osmosis-labs/osmosis/v14/x/protorev/types" ) @@ -14,136 +16,91 @@ type TestRoute struct { // TestBuildRoutes tests the BuildRoutes function func (suite *KeeperTestSuite) TestBuildRoutes() { cases := []struct { - description string - inputDenom string - outputDenom string - poolID uint64 - expected [][]TestRoute - expectedPointCount uint64 - maxPoolPoints uint64 + description string + inputDenom string + outputDenom string + poolID uint64 + expectedRoutes [][]TestRoute }{ { description: "Route exists for swap in Akash and swap out Atom", inputDenom: "akash", - outputDenom: types.AtomDenomination, + outputDenom: "Atom", poolID: 1, - expected: [][]TestRoute{ + expectedRoutes: [][]TestRoute{ { - {PoolId: 1, InputDenom: types.AtomDenomination, OutputDenom: "akash"}, + {PoolId: 1, InputDenom: "Atom", OutputDenom: "akash"}, {PoolId: 14, InputDenom: "akash", OutputDenom: "bitcoin"}, - {PoolId: 4, InputDenom: "bitcoin", OutputDenom: types.AtomDenomination}, + {PoolId: 4, InputDenom: "bitcoin", OutputDenom: "Atom"}, }, { - {PoolId: 25, InputDenom: types.OsmosisDenomination, OutputDenom: types.AtomDenomination}, - {PoolId: 1, InputDenom: types.AtomDenomination, OutputDenom: "akash"}, + {PoolId: 25, InputDenom: types.OsmosisDenomination, OutputDenom: "Atom"}, + {PoolId: 1, InputDenom: "Atom", OutputDenom: "akash"}, {PoolId: 7, InputDenom: "akash", OutputDenom: types.OsmosisDenomination}, }, }, - expectedPointCount: 12, - maxPoolPoints: 15, }, { description: "Route exists for swap in Bitcoin and swap out Atom", inputDenom: "bitcoin", - outputDenom: types.AtomDenomination, + outputDenom: "Atom", poolID: 4, - expected: [][]TestRoute{ + expectedRoutes: [][]TestRoute{ { - {PoolId: 25, InputDenom: types.OsmosisDenomination, OutputDenom: types.AtomDenomination}, - {PoolId: 4, InputDenom: types.AtomDenomination, OutputDenom: "bitcoin"}, + {PoolId: 25, InputDenom: types.OsmosisDenomination, OutputDenom: "Atom"}, + {PoolId: 4, InputDenom: "Atom", OutputDenom: "bitcoin"}, {PoolId: 10, InputDenom: "bitcoin", OutputDenom: types.OsmosisDenomination}, }, }, - expectedPointCount: 6, - maxPoolPoints: 15, }, { description: "Route exists for swap in Bitcoin and swap out ethereum", inputDenom: "bitcoin", outputDenom: "ethereum", poolID: 19, - expected: [][]TestRoute{ + expectedRoutes: [][]TestRoute{ { {PoolId: 9, InputDenom: types.OsmosisDenomination, OutputDenom: "ethereum"}, {PoolId: 19, InputDenom: "ethereum", OutputDenom: "bitcoin"}, {PoolId: 10, InputDenom: "bitcoin", OutputDenom: types.OsmosisDenomination}, }, { - {PoolId: 3, InputDenom: types.AtomDenomination, OutputDenom: "ethereum"}, + {PoolId: 3, InputDenom: "Atom", OutputDenom: "ethereum"}, {PoolId: 19, InputDenom: "ethereum", OutputDenom: "bitcoin"}, - {PoolId: 4, InputDenom: "bitcoin", OutputDenom: types.AtomDenomination}, + {PoolId: 4, InputDenom: "bitcoin", OutputDenom: "Atom"}, }, }, - expectedPointCount: 12, - maxPoolPoints: 15, }, { - description: "No route exists for swap in osmo and swap out Atom", - inputDenom: types.OsmosisDenomination, - outputDenom: types.AtomDenomination, - poolID: 25, - expected: [][]TestRoute{}, - expectedPointCount: 0, - maxPoolPoints: 15, + description: "No route exists for swap in osmo and swap out Atom", + inputDenom: types.OsmosisDenomination, + outputDenom: "Atom", + poolID: 25, + expectedRoutes: [][]TestRoute{}, }, { description: "Route exists for swap on stable pool", inputDenom: "usdc", outputDenom: types.OsmosisDenomination, poolID: 29, - expected: [][]TestRoute{ + expectedRoutes: [][]TestRoute{ { {PoolId: 29, InputDenom: types.OsmosisDenomination, OutputDenom: "usdc"}, - {PoolId: 34, InputDenom: "usdc", OutputDenom: "busd"}, + {PoolId: 40, InputDenom: "usdc", OutputDenom: "busd"}, {PoolId: 30, InputDenom: "busd", OutputDenom: types.OsmosisDenomination}, }, }, - expectedPointCount: 7, - maxPoolPoints: 15, - }, - { - description: "Route exists for swap on stable pool but not enough routes left to be explored", - inputDenom: "usdc", - outputDenom: types.OsmosisDenomination, - poolID: 29, - expected: [][]TestRoute{}, - expectedPointCount: 0, - maxPoolPoints: 3, - }, - { - description: "Two routes exist but only 1 route left to be explored (osmo route chosen)", - inputDenom: "bitcoin", - outputDenom: "ethereum", - poolID: 19, - expected: [][]TestRoute{ - { - {PoolId: 9, InputDenom: types.OsmosisDenomination, OutputDenom: "ethereum"}, - {PoolId: 19, InputDenom: "ethereum", OutputDenom: "bitcoin"}, - {PoolId: 10, InputDenom: "bitcoin", OutputDenom: types.OsmosisDenomination}, - }, - }, - expectedPointCount: 6, - maxPoolPoints: 6, }, } for _, tc := range cases { suite.Run(tc.description, func() { - suite.SetupTest() - - suite.App.ProtoRevKeeper.SetPoolWeights(suite.Ctx, types.PoolWeights{StableWeight: 3, BalancerWeight: 2, ConcentratedWeight: 1}) - - routes := suite.App.ProtoRevKeeper.BuildRoutes(suite.Ctx, tc.inputDenom, tc.outputDenom, tc.poolID, &tc.maxPoolPoints) - - suite.Require().Equal(len(tc.expected), len(routes)) - pointCount, err := suite.App.ProtoRevKeeper.GetPointCountForBlock(suite.Ctx) - suite.Require().NoError(err) - suite.Require().Equal(tc.expectedPointCount, pointCount) + routes := suite.App.ProtoRevKeeper.BuildRoutes(suite.Ctx, tc.inputDenom, tc.outputDenom, tc.poolID) + suite.Require().Equal(len(tc.expectedRoutes), len(routes)) for routeIndex, route := range routes { - for tradeIndex, trade := range route { - suite.Require().Equal(tc.expected[routeIndex][tradeIndex].PoolId, trade.PoolId) - suite.Require().Equal(tc.expected[routeIndex][tradeIndex].OutputDenom, trade.TokenOutDenom) + for tradeIndex, poolID := range route.Route.PoolIds() { + suite.Require().Equal(tc.expectedRoutes[routeIndex][tradeIndex].PoolId, poolID) } } }) @@ -153,102 +110,116 @@ func (suite *KeeperTestSuite) TestBuildRoutes() { // TestBuildHighestLiquidityRoute tests the BuildHighestLiquidityRoute function func (suite *KeeperTestSuite) TestBuildHighestLiquidityRoute() { cases := []struct { - description string - swapDenom string - swapIn string - swapOut string - poolId uint64 - expectedRoute []TestRoute - hasRoute bool - expectedPointCount uint64 + description string + swapDenom string + swapIn string + swapOut string + poolId uint64 + expectedRoute []TestRoute + hasRoute bool + expectedRoutePointPoints uint64 }{ { - description: "Route exists for swap in Atom and swap out Akash", - swapDenom: types.OsmosisDenomination, - swapIn: types.AtomDenomination, - swapOut: "akash", - poolId: 1, - expectedRoute: []TestRoute{{7, types.OsmosisDenomination, "akash"}, {1, "akash", types.AtomDenomination}, {25, types.AtomDenomination, types.OsmosisDenomination}}, - hasRoute: true, - expectedPointCount: 6, + description: "Route exists for swap in Atom and swap out Akash", + swapDenom: types.OsmosisDenomination, + swapIn: "Atom", + swapOut: "akash", + poolId: 1, + expectedRoute: []TestRoute{ + {7, types.OsmosisDenomination, "akash"}, + {1, "akash", "Atom"}, + {25, "Atom", types.OsmosisDenomination}, + }, + hasRoute: true, + expectedRoutePointPoints: 6, }, { - description: "Route exists for swap in Akash and swap out Atom", - swapDenom: types.OsmosisDenomination, - swapIn: "akash", - swapOut: types.AtomDenomination, - poolId: 1, - expectedRoute: []TestRoute{{25, types.OsmosisDenomination, types.AtomDenomination}, {1, types.AtomDenomination, "akash"}, {7, "akash", types.OsmosisDenomination}}, - hasRoute: true, - expectedPointCount: 6, + description: "Route exists for swap in Akash and swap out Atom", + swapDenom: types.OsmosisDenomination, + swapIn: "akash", + swapOut: "Atom", + poolId: 1, + expectedRoute: []TestRoute{ + {25, types.OsmosisDenomination, "Atom"}, + {1, "Atom", "akash"}, + {7, "akash", types.OsmosisDenomination}, + }, + hasRoute: true, + expectedRoutePointPoints: 6, }, { - description: "Route does not exist for swap in Terra and swap out Atom because the pool does not exist", - swapDenom: types.OsmosisDenomination, - swapIn: "terra", - swapOut: types.AtomDenomination, - poolId: 7, - expectedRoute: []TestRoute{}, - hasRoute: false, - expectedPointCount: 0, + description: "Route does not exist for swap in Terra and swap out Atom because the pool does not exist", + swapDenom: types.OsmosisDenomination, + swapIn: "terra", + swapOut: "Atom", + poolId: 7, + expectedRoute: []TestRoute{}, + hasRoute: false, + expectedRoutePointPoints: 0, }, { - description: "Route exists for swap in Osmo and swap out Akash", - swapDenom: types.AtomDenomination, - swapIn: types.OsmosisDenomination, - swapOut: "akash", - poolId: 7, - expectedRoute: []TestRoute{{1, types.AtomDenomination, "akash"}, {7, "akash", types.OsmosisDenomination}, {25, types.OsmosisDenomination, types.AtomDenomination}}, - hasRoute: true, - expectedPointCount: 6, + description: "Route exists for swap in Osmo and swap out Akash", + swapDenom: "Atom", + swapIn: types.OsmosisDenomination, + swapOut: "akash", + poolId: 7, + expectedRoute: []TestRoute{ + {1, "Atom", "akash"}, + {7, "akash", types.OsmosisDenomination}, + {25, types.OsmosisDenomination, "Atom"}, + }, + hasRoute: true, + expectedRoutePointPoints: 6, }, { - description: "Route exists for swap in Akash and swap out Osmo", - swapDenom: types.AtomDenomination, - swapIn: "akash", - swapOut: types.OsmosisDenomination, - poolId: 7, - expectedRoute: []TestRoute{{25, types.AtomDenomination, types.OsmosisDenomination}, {7, types.OsmosisDenomination, "akash"}, {1, "akash", types.AtomDenomination}}, - hasRoute: true, - expectedPointCount: 6, + description: "Route exists for swap in Akash and swap out Osmo", + swapDenom: "Atom", + swapIn: "akash", + swapOut: types.OsmosisDenomination, + poolId: 7, + expectedRoute: []TestRoute{ + {25, "Atom", types.OsmosisDenomination}, + {7, types.OsmosisDenomination, "akash"}, + {1, "akash", "Atom"}, + }, + hasRoute: true, + expectedRoutePointPoints: 6, }, { - description: "Route does not exist for swap in Terra and swap out Osmo because the pool does not exist", - swapDenom: types.AtomDenomination, - swapIn: "terra", - swapOut: types.OsmosisDenomination, - poolId: 7, - expectedRoute: []TestRoute{}, - hasRoute: false, - expectedPointCount: 0, + description: "Route does not exist for swap in Terra and swap out Osmo because the pool does not exist", + swapDenom: "Atom", + swapIn: "terra", + swapOut: types.OsmosisDenomination, + poolId: 7, + expectedRoute: []TestRoute{}, + hasRoute: false, + expectedRoutePointPoints: 0, }, } for _, tc := range cases { suite.Run(tc.description, func() { - suite.SetupTest() - - pointCount, err := suite.App.ProtoRevKeeper.RemainingPoolPointsForTx(suite.Ctx) - suite.Require().NoError(err) - before := *pointCount - - route, buildErr := suite.App.ProtoRevKeeper.BuildHighestLiquidityRoute(suite.Ctx, tc.swapDenom, tc.swapIn, tc.swapOut, tc.poolId, pointCount) - pointCountAfter, err := suite.App.ProtoRevKeeper.GetPointCountForBlock(suite.Ctx) - suite.Require().NoError(err) - - suite.Require().Equal(tc.expectedPointCount, pointCountAfter) - suite.Require().Equal(*pointCount+pointCountAfter, before) + suite.App.ProtoRevKeeper.SetPoolWeights(suite.Ctx, types.PoolWeights{ + StableWeight: 5, + BalancerWeight: 2, + ConcentratedWeight: 2, + }) + + baseDenom := types.BaseDenom{ + Denom: tc.swapDenom, + StepSize: sdk.NewInt(1_000_000), + } + routeMetaData, err := suite.App.ProtoRevKeeper.BuildHighestLiquidityRoute(suite.Ctx, &baseDenom, tc.swapIn, tc.swapOut, tc.poolId) if tc.hasRoute { - suite.Require().NoError(buildErr) - suite.Require().Equal(len(tc.expectedRoute), len(route.PoolIds())) + suite.Require().NoError(err) + suite.Require().Equal(len(tc.expectedRoute), len(routeMetaData.Route.PoolIds())) for index, trade := range tc.expectedRoute { - suite.Require().Equal(trade.PoolId, route[index].PoolId) - suite.Require().Equal(trade.OutputDenom, route[index].TokenOutDenom) + suite.Require().Equal(trade.PoolId, routeMetaData.Route.PoolIds()[index]) } } else { - suite.Require().Error(buildErr) + suite.Require().Error(err) } }) } @@ -257,44 +228,80 @@ func (suite *KeeperTestSuite) TestBuildHighestLiquidityRoute() { // TestBuildHotRoutes tests the BuildHotRoutes function func (suite *KeeperTestSuite) TestBuildHotRoutes() { cases := []struct { - description string - swapIn string - swapOut string - poolId uint64 - expectedRoutes [][]TestRoute - hasRoutes bool + description string + swapIn string + swapOut string + poolId uint64 + expectedRoutes [][]TestRoute + expectedStepSize []sdk.Int + expectedRoutePoolPoints []uint64 + hasRoutes bool }{ { - description: "Route exists for swap in Atom and swap out Akash", - swapIn: "akash", - swapOut: types.AtomDenomination, - poolId: 1, - expectedRoutes: [][]TestRoute{{{1, types.AtomDenomination, "akash"}, {14, "akash", "bitcoin"}, {4, "bitcoin", types.AtomDenomination}}}, - hasRoutes: true, + description: "Route exists for swap in Atom and swap out Akash", + swapIn: "akash", + swapOut: "Atom", + poolId: 1, + expectedRoutes: [][]TestRoute{ + { + {1, "Atom", "akash"}, + {14, "akash", "bitcoin"}, + {4, "bitcoin", "Atom"}, + }, + }, + expectedStepSize: []sdk.Int{sdk.NewInt(1_000_000)}, + expectedRoutePoolPoints: []uint64{6}, + hasRoutes: true, + }, + { + description: "Route exists for a four pool route", + swapIn: "Atom", + swapOut: "test/2", + poolId: 10, + expectedRoutes: [][]TestRoute{ + { + {34, "Atom", "test/1"}, + {35, "test/1", types.OsmosisDenomination}, + {36, types.OsmosisDenomination, "test/2"}, + {10, "test/2", "Atom"}, + }, + }, + expectedStepSize: []sdk.Int{sdk.NewInt(1_000_000)}, + expectedRoutePoolPoints: []uint64{8}, + hasRoutes: true, }, } for _, tc := range cases { suite.Run(tc.description, func() { - maxPoints, err := suite.App.ProtoRevKeeper.RemainingPoolPointsForTx(suite.Ctx) - suite.Require().NoError(err) + suite.App.ProtoRevKeeper.SetPoolWeights(suite.Ctx, types.PoolWeights{ + StableWeight: 5, + BalancerWeight: 2, + ConcentratedWeight: 2, + }) - routes, err := suite.App.ProtoRevKeeper.BuildHotRoutes(suite.Ctx, tc.swapIn, tc.swapOut, tc.poolId, maxPoints) + routes, err := suite.App.ProtoRevKeeper.BuildHotRoutes(suite.Ctx, tc.swapIn, tc.swapOut, tc.poolId) if tc.hasRoutes { suite.Require().NoError(err) suite.Require().Equal(len(tc.expectedRoutes), len(routes)) - for index, route := range routes { + for routeIndex, routeMetaData := range routes { + expectedHops := len(tc.expectedRoutes[routeIndex]) + suite.Require().Equal(expectedHops, len(routeMetaData.Route.PoolIds())) + + expectedStepSize := tc.expectedStepSize[routeIndex] + suite.Require().Equal(expectedStepSize, routeMetaData.StepSize) + + expectedPoolPoints := tc.expectedRoutePoolPoints[routeIndex] + suite.Require().Equal(expectedPoolPoints, routeMetaData.PoolPoints) - suite.Require().Equal(len(tc.expectedRoutes[index]), len(route.PoolIds())) + expectedRoutes := tc.expectedRoutes[routeIndex] - for index, trade := range tc.expectedRoutes[index] { - suite.Require().Equal(trade.PoolId, route[index].PoolId) - suite.Require().Equal(trade.OutputDenom, route[index].TokenOutDenom) + for tradeIndex, trade := range expectedRoutes { + suite.Require().Equal(trade.PoolId, routeMetaData.Route.PoolIds()[tradeIndex]) } } - } else { suite.Require().Error(err) } @@ -302,129 +309,59 @@ func (suite *KeeperTestSuite) TestBuildHotRoutes() { } } -// TestCheckAndUpdateRouteState tests the CheckAndUpdateRouteState function -func (suite *KeeperTestSuite) TestCheckAndUpdateRouteState() { +// TestCalculateRoutePoolPoints tests the CalculateRoutePoolPoints function +func (suite *KeeperTestSuite) TestCalculateRoutePoolPoints() { cases := []struct { - description string - route poolmanagertypes.SwapAmountInRoutes - maxPoolPoints uint64 - expectedRemainingPoolPoints uint64 - expectedPass bool + description string + route poolmanagertypes.SwapAmountInRoutes + expectedRoutePoolPoints uint64 + expectedPass bool }{ { - description: "Valid route containing only balancer pools", - route: []poolmanagertypes.SwapAmountInRoute{{PoolId: 1, TokenOutDenom: ""}, {PoolId: 2, TokenOutDenom: ""}, {PoolId: 3, TokenOutDenom: ""}}, - maxPoolPoints: 10, - expectedRemainingPoolPoints: 4, - expectedPass: true, - }, - { - description: "Valid route containing only balancer pools but not enough pool points", - route: []poolmanagertypes.SwapAmountInRoute{{PoolId: 1, TokenOutDenom: ""}, {PoolId: 2, TokenOutDenom: ""}, {PoolId: 3, TokenOutDenom: ""}}, - maxPoolPoints: 2, - expectedRemainingPoolPoints: 2, - expectedPass: false, + description: "Valid route containing only balancer pools", + route: []poolmanagertypes.SwapAmountInRoute{{PoolId: 1, TokenOutDenom: ""}, {PoolId: 2, TokenOutDenom: ""}, {PoolId: 3, TokenOutDenom: ""}}, + expectedRoutePoolPoints: 6, + expectedPass: true, }, { - description: "Valid route containing only balancer pools and equal number of pool points", - route: []poolmanagertypes.SwapAmountInRoute{{PoolId: 1, TokenOutDenom: ""}, {PoolId: 2, TokenOutDenom: ""}, {PoolId: 3, TokenOutDenom: ""}}, - maxPoolPoints: 6, - expectedRemainingPoolPoints: 0, - expectedPass: true, + description: "Valid route containing only balancer pools and equal number of pool points", + route: []poolmanagertypes.SwapAmountInRoute{{PoolId: 1, TokenOutDenom: ""}, {PoolId: 2, TokenOutDenom: ""}, {PoolId: 3, TokenOutDenom: ""}}, + expectedRoutePoolPoints: 6, + expectedPass: true, }, { - description: "Valid route containing only stable swap pools", - route: []poolmanagertypes.SwapAmountInRoute{{PoolId: 34, TokenOutDenom: ""}, {PoolId: 34, TokenOutDenom: ""}, {PoolId: 34, TokenOutDenom: ""}}, - maxPoolPoints: 10, - expectedRemainingPoolPoints: 1, - expectedPass: true, + description: "Valid route containing only stable swap pools", + route: []poolmanagertypes.SwapAmountInRoute{{PoolId: 40, TokenOutDenom: ""}, {PoolId: 40, TokenOutDenom: ""}, {PoolId: 40, TokenOutDenom: ""}}, + expectedRoutePoolPoints: 9, + expectedPass: true, }, { - description: "Valid route with more than 3 hops", - route: []poolmanagertypes.SwapAmountInRoute{{PoolId: 34, TokenOutDenom: ""}, {PoolId: 34, TokenOutDenom: ""}, {PoolId: 34, TokenOutDenom: ""}, {PoolId: 1, TokenOutDenom: ""}}, - maxPoolPoints: 12, - expectedRemainingPoolPoints: 1, - expectedPass: true, + description: "Valid route with more than 3 hops", + route: []poolmanagertypes.SwapAmountInRoute{{PoolId: 40, TokenOutDenom: ""}, {PoolId: 40, TokenOutDenom: ""}, {PoolId: 40, TokenOutDenom: ""}, {PoolId: 1, TokenOutDenom: ""}}, + expectedRoutePoolPoints: 11, + expectedPass: true, }, { - description: "Valid route with more than 3 hops", - route: []poolmanagertypes.SwapAmountInRoute{{PoolId: 34, TokenOutDenom: ""}, {PoolId: 34, TokenOutDenom: ""}, {PoolId: 34, TokenOutDenom: ""}, {PoolId: 1, TokenOutDenom: ""}, {PoolId: 2, TokenOutDenom: ""}, {PoolId: 3, TokenOutDenom: ""}}, - maxPoolPoints: 12, - expectedRemainingPoolPoints: 12, - expectedPass: false, + description: "Invalid route with more than 3 hops", + route: []poolmanagertypes.SwapAmountInRoute{{PoolId: 4000, TokenOutDenom: ""}, {PoolId: 40, TokenOutDenom: ""}, {PoolId: 40, TokenOutDenom: ""}, {PoolId: 1, TokenOutDenom: ""}}, + expectedRoutePoolPoints: 11, + expectedPass: false, }, } for _, tc := range cases { suite.Run(tc.description, func() { suite.SetupTest() - suite.App.ProtoRevKeeper.SetPoolWeights(suite.Ctx, types.PoolWeights{StableWeight: 3, BalancerWeight: 2, ConcentratedWeight: 1}) - var maxPoints *uint64 = &tc.maxPoolPoints - - err := suite.App.ProtoRevKeeper.CheckAndUpdateRouteState(suite.Ctx, tc.route, maxPoints) + routePoolPoints, err := suite.App.ProtoRevKeeper.CalculateRoutePoolPoints(suite.Ctx, tc.route) if tc.expectedPass { suite.Require().NoError(err) + suite.Require().Equal(tc.expectedRoutePoolPoints, routePoolPoints) } else { suite.Require().Error(err) } - suite.Require().Equal(tc.expectedRemainingPoolPoints, tc.maxPoolPoints) - }) - } -} - -// TestRemainingPoolPointsForTx tests the RemainingPoolPointsForTx function. -func (suite *KeeperTestSuite) TestRemainingPoolPointsForTx() { - cases := []struct { - description string - maxRoutesPerTx uint64 - maxRoutesPerBlock uint64 - currentRouteCount uint64 - expectedPointCount uint64 - }{ - { - description: "Max pool points per tx is 10 and max pool points per block is 100", - maxRoutesPerTx: 10, - maxRoutesPerBlock: 100, - currentRouteCount: 0, - expectedPointCount: 10, - }, - { - description: "Max pool points per tx is 10, max pool points per block is 100, and current point count is 90", - maxRoutesPerTx: 10, - maxRoutesPerBlock: 100, - currentRouteCount: 90, - expectedPointCount: 10, - }, - { - description: "Max pool points per tx is 10, max pool points per block is 100, and current point count is 100", - maxRoutesPerTx: 10, - maxRoutesPerBlock: 100, - currentRouteCount: 100, - expectedPointCount: 0, - }, - { - description: "Max pool points per tx is 10, max pool points per block is 100, and current point count is 95", - maxRoutesPerTx: 10, - maxRoutesPerBlock: 100, - currentRouteCount: 95, - expectedPointCount: 5, - }, - } - - for _, tc := range cases { - suite.Run(tc.description, func() { - suite.SetupTest() - - suite.App.ProtoRevKeeper.SetMaxPointsPerTx(suite.Ctx, tc.maxRoutesPerTx) - suite.App.ProtoRevKeeper.SetMaxPointsPerBlock(suite.Ctx, tc.maxRoutesPerBlock) - suite.App.ProtoRevKeeper.SetPointCountForBlock(suite.Ctx, tc.currentRouteCount) - - points, err := suite.App.ProtoRevKeeper.RemainingPoolPointsForTx(suite.Ctx) - suite.Require().NoError(err) - suite.Require().Equal(tc.expectedPointCount, *points) }) } } diff --git a/x/protorev/keeper/statistics_test.go b/x/protorev/keeper/statistics_test.go index ec3003d6067..b22fc2d2d49 100644 --- a/x/protorev/keeper/statistics_test.go +++ b/x/protorev/keeper/statistics_test.go @@ -54,12 +54,12 @@ func (suite *KeeperTestSuite) TestGetProfitsByDenom() { suite.Require().Equal([]*sdk.Coin{{Denom: types.OsmosisDenomination, Amount: sdk.NewInt(14000)}}, allProfits) // Pseudo execute a third trade in a different denom - err = suite.App.ProtoRevKeeper.UpdateProfitsByDenom(suite.Ctx, types.AtomDenomination, sdk.NewInt(1000)) + err = suite.App.ProtoRevKeeper.UpdateProfitsByDenom(suite.Ctx, "Atom", sdk.NewInt(1000)) suite.Require().NoError(err) // Check the result of GetAllProfits allProfits = suite.App.ProtoRevKeeper.GetAllProfits(suite.Ctx) - suite.Require().Equal([]*sdk.Coin{{Denom: types.AtomDenomination, Amount: sdk.NewInt(1000)}, {Denom: types.OsmosisDenomination, Amount: sdk.NewInt(14000)}}, allProfits) + suite.Require().Equal([]*sdk.Coin{{Denom: "Atom", Amount: sdk.NewInt(1000)}, {Denom: types.OsmosisDenomination, Amount: sdk.NewInt(14000)}}, allProfits) } // TestGetTradesByRoute tests GetTradesByRoute, IncrementTradesByRoute, and GetAllRoutes @@ -131,18 +131,18 @@ func (suite *KeeperTestSuite) TestGetProfitsByRoute() { suite.Require().Equal([]*sdk.Coin{{Denom: types.OsmosisDenomination, Amount: sdk.NewInt(1000)}}, profits) // Pseudo execute a second trade - err = suite.App.ProtoRevKeeper.UpdateProfitsByRoute(suite.Ctx, []uint64{1, 2, 3}, types.AtomDenomination, sdk.NewInt(2000)) + err = suite.App.ProtoRevKeeper.UpdateProfitsByRoute(suite.Ctx, []uint64{1, 2, 3}, "Atom", sdk.NewInt(2000)) suite.Require().NoError(err) // Check the updated result after the second trade - profit, err = suite.App.ProtoRevKeeper.GetProfitsByRoute(suite.Ctx, []uint64{1, 2, 3}, types.AtomDenomination) + profit, err = suite.App.ProtoRevKeeper.GetProfitsByRoute(suite.Ctx, []uint64{1, 2, 3}, "Atom") suite.Require().NoError(err) - suite.Require().Equal(sdk.NewCoin(types.AtomDenomination, sdk.NewInt(2000)), profit) + suite.Require().Equal(sdk.NewCoin("Atom", sdk.NewInt(2000)), profit) // Check the result of GetAllProfitsByRoute profits = suite.App.ProtoRevKeeper.GetAllProfitsByRoute(suite.Ctx, []uint64{1, 2, 3}) suite.Require().Contains(profits, &sdk.Coin{Denom: types.OsmosisDenomination, Amount: sdk.NewInt(1000)}) - suite.Require().Contains(profits, &sdk.Coin{Denom: types.AtomDenomination, Amount: sdk.NewInt(2000)}) + suite.Require().Contains(profits, &sdk.Coin{Denom: "Atom", Amount: sdk.NewInt(2000)}) } // TestUpdateStatistics tests UpdateStatistics which is a wrapper for much of the statistics keeper diff --git a/x/protorev/types/constants.go b/x/protorev/types/constants.go index 438266a31e1..f2974579e0a 100644 --- a/x/protorev/types/constants.go +++ b/x/protorev/types/constants.go @@ -4,9 +4,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// AtomDenomination stores the native denom name for Atom on chain used for route building -var AtomDenomination string = "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2" - // OsmosisDenomination stores the native denom name for Osmosis on chain used for route building var OsmosisDenomination string = "uosmo" @@ -15,11 +12,12 @@ var OsmosisDenomination string = "uosmo" // MaxInputAmount is the upper bound index for finding the optimal in amount when determining route profitability (2 ^ 14) = 16,384 var MaxInputAmount = sdk.NewInt(16_384) -// StepSize is the amount we multiply each index in the binary search method -var StepSize = sdk.NewInt(1_000_000) +// ExtendedMaxInputAmount is the upper bound index for finding the optimal in amount +// when determining route profitability for an arb that's above the default range (2 ^ 17) = 131,072 +var ExtendedMaxInputAmount = sdk.NewInt(131_072) -// Max iterations for binary search (log2(16_384) = 14) -const MaxIterations int = 14 +// Max iterations for binary search (log2(131_072) = 17) +const MaxIterations int = 17 // Max number of pool points that can be consumed per tx. This roughly corresponds // to the maximum execution time (in ms) of protorev per tx diff --git a/x/protorev/types/genesis.go b/x/protorev/types/genesis.go index ea932e3a8f1..c2cdd03074c 100644 --- a/x/protorev/types/genesis.go +++ b/x/protorev/types/genesis.go @@ -1,9 +1,5 @@ package types -import ( - "fmt" -) - type TokenPair struct { TokenA string TokenB string @@ -12,47 +8,15 @@ type TokenPair struct { // DefaultGenesis returns the default genesis state func DefaultGenesis() *GenesisState { return &GenesisState{ - Params: DefaultParams(), - TokenPairs: []TokenPairArbRoutes{}, + Params: DefaultParams(), } } // Validate performs basic genesis state validation returning an error upon any failure. func (gs GenesisState) Validate() error { - // Validate entered routes - if err := gs.CheckRoutes(); err != nil { - return err - } - return gs.Params.Validate() } func init() { // no-op } - -// Routes entered into the genesis state must start and end with the same denomination and -// the denomination must be Osmo or Atom. Additionally, there cannot be duplicate routes (same -// token pairs). -func (gs GenesisState) CheckRoutes() error { - seenTokenPairs := make(map[TokenPair]bool) - for _, tokenPairArbRoutes := range gs.TokenPairs { - // Validate the arb routes - if err := tokenPairArbRoutes.Validate(); err != nil { - return err - } - - tokenPair := TokenPair{ - TokenA: tokenPairArbRoutes.TokenIn, - TokenB: tokenPairArbRoutes.TokenOut, - } - // Validate that the token pair is unique - if _, ok := seenTokenPairs[tokenPair]; ok { - return fmt.Errorf("duplicate token pair: %s", tokenPair) - } - - seenTokenPairs[tokenPair] = true - } - - return nil -} diff --git a/x/protorev/types/genesis.pb.go b/x/protorev/types/genesis.pb.go index 6005898af4e..458bd5f7c97 100644 --- a/x/protorev/types/genesis.pb.go +++ b/x/protorev/types/genesis.pb.go @@ -27,8 +27,6 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package type GenesisState struct { // Module Parameters Params Params `protobuf:"bytes,1,opt,name=params,proto3" json:"params"` - // Hot routes that are configured on genesis - TokenPairs []TokenPairArbRoutes `protobuf:"bytes,2,rep,name=token_pairs,json=tokenPairs,proto3" json:"token_pairs"` } func (m *GenesisState) Reset() { *m = GenesisState{} } @@ -71,13 +69,6 @@ func (m *GenesisState) GetParams() Params { return Params{} } -func (m *GenesisState) GetTokenPairs() []TokenPairArbRoutes { - if m != nil { - return m.TokenPairs - } - return nil -} - func init() { proto.RegisterType((*GenesisState)(nil), "osmosis.protorev.v1beta1.GenesisState") } @@ -87,24 +78,21 @@ func init() { } var fileDescriptor_3c77fc2da5752af2 = []byte{ - // 260 bytes of a gzipped FileDescriptorProto + // 212 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0xcb, 0x2f, 0xce, 0xcd, 0x2f, 0xce, 0x2c, 0xd6, 0x2f, 0x28, 0xca, 0x2f, 0xc9, 0x2f, 0x4a, 0x2d, 0xd3, 0x2f, 0x33, 0x4c, 0x4a, 0x2d, 0x49, 0x34, 0xd4, 0x4f, 0x4f, 0xcd, 0x4b, 0x2d, 0xce, 0x2c, 0xd6, 0x03, 0x4b, 0x08, 0x49, 0x40, 0xd5, 0xe9, 0xc1, 0xd4, 0xe9, 0x41, 0xd5, 0x49, 0x89, 0xa4, 0xe7, 0xa7, 0xe7, 0x83, 0x45, 0xf5, 0x41, 0x2c, 0x88, 0x02, 0x29, 0x75, 0x9c, 0xe6, 0xc2, 0x0d, 0x80, 0x28, 0x54, 0xc5, - 0xad, 0x30, 0xb1, 0x28, 0x31, 0x17, 0x6a, 0xa1, 0xd2, 0x62, 0x46, 0x2e, 0x1e, 0x77, 0x88, 0x8b, - 0x82, 0x4b, 0x12, 0x4b, 0x52, 0x85, 0xec, 0xb8, 0xd8, 0x20, 0x0a, 0x24, 0x18, 0x15, 0x18, 0x35, - 0xb8, 0x8d, 0x14, 0xf4, 0x70, 0xb9, 0x50, 0x2f, 0x00, 0xac, 0xce, 0x89, 0xe5, 0xc4, 0x3d, 0x79, - 0x86, 0x20, 0xa8, 0x2e, 0xa1, 0x60, 0x2e, 0xee, 0x92, 0xfc, 0xec, 0xd4, 0xbc, 0xf8, 0x82, 0xc4, - 0xcc, 0xa2, 0x62, 0x09, 0x26, 0x05, 0x66, 0x0d, 0x6e, 0x23, 0x1d, 0xdc, 0x86, 0x84, 0x80, 0x14, - 0x07, 0x24, 0x66, 0x16, 0x39, 0x16, 0x25, 0x05, 0xe5, 0x97, 0x96, 0xa4, 0xc2, 0x0c, 0xe4, 0x2a, - 0x81, 0xc9, 0x14, 0x3b, 0xf9, 0x9d, 0x78, 0x24, 0xc7, 0x78, 0xe1, 0x91, 0x1c, 0xe3, 0x83, 0x47, - 0x72, 0x8c, 0x13, 0x1e, 0xcb, 0x31, 0x5c, 0x78, 0x2c, 0xc7, 0x70, 0xe3, 0xb1, 0x1c, 0x43, 0x94, - 0x49, 0x7a, 0x66, 0x49, 0x46, 0x69, 0x92, 0x5e, 0x72, 0x7e, 0xae, 0x3e, 0xd4, 0x0e, 0xdd, 0x9c, - 0xc4, 0xa4, 0x62, 0x18, 0x47, 0xbf, 0xcc, 0xd0, 0x44, 0xbf, 0x02, 0x11, 0x08, 0x25, 0x95, 0x05, - 0xa9, 0xc5, 0x49, 0x6c, 0x60, 0xbe, 0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0xb6, 0xc2, 0x9c, 0xcb, - 0xa6, 0x01, 0x00, 0x00, + 0xad, 0x30, 0xb1, 0x28, 0x31, 0x17, 0x6a, 0xa1, 0x92, 0x1f, 0x17, 0x8f, 0x3b, 0xc4, 0x41, 0xc1, + 0x25, 0x89, 0x25, 0xa9, 0x42, 0x76, 0x5c, 0x6c, 0x10, 0x79, 0x09, 0x46, 0x05, 0x46, 0x0d, 0x6e, + 0x23, 0x05, 0x3d, 0x5c, 0x0e, 0xd4, 0x0b, 0x00, 0xab, 0x73, 0x62, 0x39, 0x71, 0x4f, 0x9e, 0x21, + 0x08, 0xaa, 0xcb, 0xc9, 0xef, 0xc4, 0x23, 0x39, 0xc6, 0x0b, 0x8f, 0xe4, 0x18, 0x1f, 0x3c, 0x92, + 0x63, 0x9c, 0xf0, 0x58, 0x8e, 0xe1, 0xc2, 0x63, 0x39, 0x86, 0x1b, 0x8f, 0xe5, 0x18, 0xa2, 0x4c, + 0xd2, 0x33, 0x4b, 0x32, 0x4a, 0x93, 0xf4, 0x92, 0xf3, 0x73, 0xf5, 0xa1, 0x66, 0xea, 0xe6, 0x24, + 0x26, 0x15, 0xc3, 0x38, 0xfa, 0x65, 0x86, 0x26, 0xfa, 0x15, 0x08, 0xe7, 0x96, 0x54, 0x16, 0xa4, + 0x16, 0x27, 0xb1, 0x81, 0xf9, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0x99, 0x06, 0x8b, 0x2b, + 0x50, 0x01, 0x00, 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { @@ -127,20 +115,6 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l - if len(m.TokenPairs) > 0 { - for iNdEx := len(m.TokenPairs) - 1; iNdEx >= 0; iNdEx-- { - { - size, err := m.TokenPairs[iNdEx].MarshalToSizedBuffer(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarintGenesis(dAtA, i, uint64(size)) - } - i-- - dAtA[i] = 0x12 - } - } { size, err := m.Params.MarshalToSizedBuffer(dAtA[:i]) if err != nil { @@ -173,12 +147,6 @@ func (m *GenesisState) Size() (n int) { _ = l l = m.Params.Size() n += 1 + l + sovGenesis(uint64(l)) - if len(m.TokenPairs) > 0 { - for _, e := range m.TokenPairs { - l = e.Size() - n += 1 + l + sovGenesis(uint64(l)) - } - } return n } @@ -250,40 +218,6 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field TokenPairs", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowGenesis - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLengthGenesis - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLengthGenesis - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.TokenPairs = append(m.TokenPairs, TokenPairArbRoutes{}) - if err := m.TokenPairs[len(m.TokenPairs)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenesis(dAtA[iNdEx:]) diff --git a/x/protorev/types/genesis_test.go b/x/protorev/types/genesis_test.go index 48da7f54083..f43ab623845 100644 --- a/x/protorev/types/genesis_test.go +++ b/x/protorev/types/genesis_test.go @@ -21,167 +21,6 @@ func TestGenesisStateValidate(t *testing.T) { }, valid: true, }, - { - description: "Default parameters with valid routes", - genState: &types.GenesisState{ - Params: types.DefaultParams(), - TokenPairs: []types.TokenPairArbRoutes{ - { - ArbRoutes: []*types.Route{{ - Trades: []*types.Trade{ - { - Pool: 1, - TokenIn: types.AtomDenomination, - TokenOut: "Juno", - }, - { - Pool: 0, - TokenIn: "Juno", - TokenOut: types.OsmosisDenomination, - }, - { - Pool: 3, - TokenIn: types.OsmosisDenomination, - TokenOut: types.AtomDenomination, - }, - }, - }}, - TokenIn: types.OsmosisDenomination, - TokenOut: "Juno", - }, - }, - }, - valid: true, - }, - { - description: "Default parameters with invalid routes (duplicate token pairs)", - genState: &types.GenesisState{ - Params: types.DefaultParams(), - TokenPairs: []types.TokenPairArbRoutes{ - { - ArbRoutes: []*types.Route{ - { - Trades: []*types.Trade{ - { - Pool: 1, - TokenIn: types.AtomDenomination, - TokenOut: "Juno", - }, - { - Pool: 0, - TokenIn: "Juno", - TokenOut: types.OsmosisDenomination, - }, - { - Pool: 3, - TokenIn: types.OsmosisDenomination, - TokenOut: types.AtomDenomination, - }, - }, - }, - }, - TokenIn: types.OsmosisDenomination, - TokenOut: "Juno", - }, - { - ArbRoutes: []*types.Route{ - { - Trades: []*types.Trade{ - { - Pool: 1, - TokenIn: types.AtomDenomination, - TokenOut: "Juno", - }, - { - Pool: 0, - TokenIn: "Juno", - TokenOut: types.OsmosisDenomination, - }, - { - Pool: 3, - TokenIn: types.OsmosisDenomination, - TokenOut: types.AtomDenomination, - }, - }, - }, - }, - TokenIn: types.OsmosisDenomination, - TokenOut: "Juno", - }, - }, - }, - valid: false, - }, - { - description: "Default parameters with nil routes", - genState: &types.GenesisState{ - Params: types.DefaultParams(), - TokenPairs: []types.TokenPairArbRoutes{ - { - ArbRoutes: nil, - TokenIn: types.OsmosisDenomination, - TokenOut: "Juno", - }, - }, - }, - valid: false, - }, - { - description: "Default parameters with invalid routes (too few trades in a route)", - genState: &types.GenesisState{ - Params: types.DefaultParams(), - TokenPairs: []types.TokenPairArbRoutes{ - { - ArbRoutes: []*types.Route{ - { - Trades: []*types.Trade{ - { - Pool: 3, - TokenIn: types.OsmosisDenomination, - TokenOut: types.AtomDenomination, - }, - }, - }, - }, - TokenIn: types.OsmosisDenomination, - TokenOut: "Juno", - }, - }, - }, - valid: false, - }, - { - description: "Default parameters with invalid routes (mismatch in and out denoms)", - genState: &types.GenesisState{ - Params: types.DefaultParams(), - TokenPairs: []types.TokenPairArbRoutes{ - { - ArbRoutes: []*types.Route{{ - Trades: []*types.Trade{ - { - Pool: 1, - TokenIn: types.AtomDenomination, - TokenOut: "Juno", - }, - { - Pool: 0, - TokenIn: "Juno", - TokenOut: types.OsmosisDenomination, - }, - { - Pool: 3, - TokenIn: types.OsmosisDenomination, - TokenOut: "eth", - }, - }, - }}, - TokenIn: types.OsmosisDenomination, - TokenOut: "Juno", - }, - }, - }, - valid: false, - }, } for _, tc := range cases { diff --git a/x/protorev/types/msg_test.go b/x/protorev/types/msg_test.go index dc9967896e7..3181a4100b6 100644 --- a/x/protorev/types/msg_test.go +++ b/x/protorev/types/msg_test.go @@ -11,6 +11,8 @@ import ( ) func TestMsgSetHotRoutes(t *testing.T) { + validStepSize := sdk.NewInt(1_000_000) + invalidStepSize := sdk.NewInt(0) cases := []struct { description string admin string @@ -39,7 +41,7 @@ func TestMsgSetHotRoutes(t *testing.T) { Trades: []*types.Trade{ { Pool: 1, - TokenIn: types.AtomDenomination, + TokenIn: "Atom", TokenOut: "Juno", }, { @@ -50,13 +52,14 @@ func TestMsgSetHotRoutes(t *testing.T) { { Pool: 3, TokenIn: types.OsmosisDenomination, - TokenOut: types.AtomDenomination, + TokenOut: "Atom", }, }, }, }, TokenIn: types.OsmosisDenomination, TokenOut: "Juno", + StepSize: &validStepSize, }, }, true, @@ -71,7 +74,7 @@ func TestMsgSetHotRoutes(t *testing.T) { Trades: []*types.Trade{ { Pool: 1, - TokenIn: types.AtomDenomination, + TokenIn: "Atom", TokenOut: "Juno", }, { @@ -89,6 +92,7 @@ func TestMsgSetHotRoutes(t *testing.T) { }, TokenIn: types.OsmosisDenomination, TokenOut: "Juno", + StepSize: &validStepSize, }, }, false, @@ -103,7 +107,7 @@ func TestMsgSetHotRoutes(t *testing.T) { Trades: []*types.Trade{ { Pool: 1, - TokenIn: types.AtomDenomination, + TokenIn: "Atom", TokenOut: "Juno", }, { @@ -114,13 +118,14 @@ func TestMsgSetHotRoutes(t *testing.T) { { Pool: 3, TokenIn: types.OsmosisDenomination, - TokenOut: types.AtomDenomination, + TokenOut: "Atom", }, }, }, }, TokenIn: types.OsmosisDenomination, TokenOut: "Juno", + StepSize: &validStepSize, }, { ArbRoutes: []*types.Route{ @@ -128,7 +133,7 @@ func TestMsgSetHotRoutes(t *testing.T) { Trades: []*types.Trade{ { Pool: 1, - TokenIn: types.AtomDenomination, + TokenIn: "Atom", TokenOut: "Juno", }, { @@ -139,13 +144,14 @@ func TestMsgSetHotRoutes(t *testing.T) { { Pool: 3, TokenIn: types.OsmosisDenomination, - TokenOut: types.AtomDenomination, + TokenOut: "Atom", }, }, }, }, TokenIn: types.OsmosisDenomination, TokenOut: "Juno", + StepSize: &validStepSize, }, }, false, @@ -160,19 +166,20 @@ func TestMsgSetHotRoutes(t *testing.T) { Trades: []*types.Trade{ { Pool: 1, - TokenIn: types.AtomDenomination, + TokenIn: "Atom", TokenOut: "Juno", }, { Pool: 3, TokenIn: types.OsmosisDenomination, - TokenOut: types.AtomDenomination, + TokenOut: "Atom", }, }, }, }, TokenIn: types.OsmosisDenomination, TokenOut: "Juno", + StepSize: &validStepSize, }, }, false, @@ -188,13 +195,14 @@ func TestMsgSetHotRoutes(t *testing.T) { { Pool: 3, TokenIn: types.OsmosisDenomination, - TokenOut: types.AtomDenomination, + TokenOut: "Atom", }, }, }, }, TokenIn: types.OsmosisDenomination, TokenOut: "Juno", + StepSize: &validStepSize, }, }, false, @@ -209,7 +217,7 @@ func TestMsgSetHotRoutes(t *testing.T) { Trades: []*types.Trade{ { Pool: 1, - TokenIn: types.AtomDenomination, + TokenIn: "Atom", TokenOut: "Juno", }, { @@ -220,7 +228,7 @@ func TestMsgSetHotRoutes(t *testing.T) { { Pool: 3, TokenIn: types.OsmosisDenomination, - TokenOut: types.AtomDenomination, + TokenOut: "Atom", }, }, }, @@ -241,6 +249,7 @@ func TestMsgSetHotRoutes(t *testing.T) { }, TokenIn: types.OsmosisDenomination, TokenOut: "Juno", + StepSize: &validStepSize, }, }, true, @@ -256,12 +265,12 @@ func TestMsgSetHotRoutes(t *testing.T) { { Pool: 3, TokenIn: types.OsmosisDenomination, - TokenOut: types.AtomDenomination, + TokenOut: "Atom", }, { Pool: 0, TokenIn: "Juno", - TokenOut: types.AtomDenomination, + TokenOut: "Atom", }, { Pool: 10, @@ -271,8 +280,74 @@ func TestMsgSetHotRoutes(t *testing.T) { }, }, }, - TokenIn: types.AtomDenomination, + TokenIn: "Atom", TokenOut: "Juno", + StepSize: &validStepSize, + }, + }, + false, + }, + { + "Invalid message (unset step size)", + createAccount().String(), + []*types.TokenPairArbRoutes{ + { + ArbRoutes: []*types.Route{ + { + Trades: []*types.Trade{ + { + Pool: 3, + TokenIn: types.OsmosisDenomination, + TokenOut: "Atom", + }, + { + Pool: 0, + TokenIn: "Juno", + TokenOut: "Atom", + }, + { + Pool: 10, + TokenIn: "Akash", + TokenOut: types.OsmosisDenomination, + }, + }, + }, + }, + TokenIn: "Atom", + TokenOut: "Juno", + }, + }, + false, + }, + { + "Invalid message (invalid step size)", + createAccount().String(), + []*types.TokenPairArbRoutes{ + { + ArbRoutes: []*types.Route{ + { + Trades: []*types.Trade{ + { + Pool: 3, + TokenIn: types.OsmosisDenomination, + TokenOut: "Atom", + }, + { + Pool: 0, + TokenIn: "Juno", + TokenOut: "Atom", + }, + { + Pool: 10, + TokenIn: "Akash", + TokenOut: types.OsmosisDenomination, + }, + }, + }, + }, + TokenIn: "Atom", + TokenOut: "Juno", + StepSize: &invalidStepSize, }, }, false, @@ -500,7 +575,7 @@ func TestMsgSetBaseDenoms(t *testing.T) { createAccount().String(), []*types.BaseDenom{ { - Denom: types.AtomDenomination, + Denom: "Atom", StepSize: sdk.NewInt(10), }, }, @@ -552,7 +627,7 @@ func TestMsgSetBaseDenoms(t *testing.T) { StepSize: sdk.NewInt(1), }, { - Denom: types.AtomDenomination, + Denom: "Atom", StepSize: sdk.NewInt(1), }, { diff --git a/x/protorev/types/protorev.pb.go b/x/protorev/types/protorev.pb.go index f8759b444df..4b6282c9032 100644 --- a/x/protorev/types/protorev.pb.go +++ b/x/protorev/types/protorev.pb.go @@ -34,6 +34,9 @@ type TokenPairArbRoutes struct { TokenIn string `protobuf:"bytes,2,opt,name=token_in,json=tokenIn,proto3" json:"token_in,omitempty"` // Token denomination of the second asset TokenOut string `protobuf:"bytes,3,opt,name=token_out,json=tokenOut,proto3" json:"token_out,omitempty"` + // The step size that will be used to find the optimal swap amount in the + // binary search + StepSize *github_com_cosmos_cosmos_sdk_types.Int `protobuf:"bytes,4,opt,name=step_size,json=stepSize,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Int" json:"step_size,omitempty"` } func (m *TokenPairArbRoutes) Reset() { *m = TokenPairArbRoutes{} } @@ -394,41 +397,42 @@ func init() { } var fileDescriptor_1e9f2391fd9fec01 = []byte{ - // 535 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x93, 0xcf, 0x6e, 0x13, 0x3d, - 0x14, 0xc5, 0xe3, 0x26, 0x69, 0x3b, 0xee, 0xf7, 0xb5, 0xc8, 0x74, 0x31, 0x29, 0xd2, 0x24, 0x0a, - 0x12, 0xcd, 0xa6, 0x33, 0x0a, 0xad, 0x84, 0xc4, 0x02, 0x89, 0x80, 0x90, 0x2a, 0x24, 0x5a, 0x4d, - 0x2b, 0xf1, 0x67, 0x33, 0xb2, 0x27, 0x4e, 0x6a, 0x35, 0xb1, 0x23, 0xdb, 0x09, 0xd0, 0x67, 0x60, - 0x81, 0x78, 0x02, 0x9e, 0x82, 0x67, 0xe8, 0xb2, 0x4b, 0xc4, 0xa2, 0x42, 0xc9, 0x86, 0xc7, 0x40, - 0x73, 0xed, 0x09, 0xdd, 0x20, 0x21, 0x58, 0xcd, 0xbd, 0xe7, 0xfe, 0x7c, 0xe6, 0xd8, 0x9e, 0xc1, - 0xbb, 0xca, 0x8c, 0x95, 0x11, 0x26, 0x99, 0x68, 0x65, 0x95, 0xe6, 0xb3, 0x64, 0xd6, 0x65, 0xdc, - 0xd2, 0xee, 0x52, 0x88, 0xa1, 0x20, 0xa1, 0x07, 0xe3, 0xa5, 0xee, 0xc1, 0x9d, 0x46, 0x0e, 0xa3, - 0x0c, 0x06, 0x89, 0x6b, 0x1c, 0xb5, 0xb3, 0x3d, 0x54, 0x43, 0xe5, 0xf4, 0xa2, 0xf2, 0x6a, 0xe4, - 0x98, 0x84, 0x51, 0xc3, 0x97, 0xaf, 0xcb, 0x95, 0x90, 0x6e, 0xde, 0xfe, 0x84, 0x30, 0x39, 0x55, - 0xe7, 0x5c, 0x1e, 0x53, 0xa1, 0x1f, 0x6b, 0x96, 0xaa, 0xa9, 0xe5, 0x86, 0x3c, 0xc2, 0x98, 0x6a, - 0x96, 0x69, 0xe8, 0x42, 0xd4, 0xaa, 0x76, 0x36, 0xee, 0x37, 0xe3, 0xdf, 0xc5, 0x8a, 0x61, 0x55, - 0x1a, 0xd0, 0xe5, 0xfa, 0x06, 0x5e, 0xb7, 0x85, 0x6b, 0x26, 0x64, 0xb8, 0xd2, 0x42, 0x9d, 0x20, - 0x5d, 0x83, 0xfe, 0x50, 0x92, 0x3b, 0x38, 0x70, 0x23, 0x35, 0xb5, 0x61, 0x15, 0x66, 0x8e, 0x3d, - 0x9a, 0xda, 0x87, 0xb5, 0x1f, 0x9f, 0x9b, 0xa8, 0xfd, 0x0c, 0xd7, 0xc1, 0x87, 0x3c, 0xc0, 0xab, - 0x56, 0xd3, 0xfe, 0x9f, 0x44, 0x38, 0x2d, 0xb8, 0xd4, 0xe3, 0xde, 0xe7, 0x35, 0xae, 0x83, 0x4c, - 0x08, 0xae, 0x4d, 0x94, 0x1a, 0x85, 0xa8, 0x85, 0x3a, 0xb5, 0x14, 0xea, 0x7f, 0x8c, 0xf8, 0x05, - 0xe1, 0x2d, 0xc8, 0x78, 0x62, 0xa9, 0x15, 0xc6, 0x8a, 0xdc, 0x90, 0x7d, 0xbc, 0x36, 0xd1, 0x6a, - 0x20, 0x6c, 0x19, 0xb7, 0x11, 0xfb, 0x1b, 0x2a, 0x4e, 0x7f, 0x99, 0xf4, 0x89, 0x12, 0x32, 0x2d, - 0x49, 0xf2, 0x0a, 0xdf, 0x92, 0xd3, 0x31, 0xe3, 0x3a, 0x53, 0x83, 0xcc, 0x6f, 0x16, 0xe2, 0xf4, - 0xe2, 0xcb, 0xeb, 0x66, 0xe5, 0xdb, 0x75, 0xf3, 0xde, 0x50, 0xd8, 0xb3, 0x29, 0x8b, 0x73, 0x35, - 0xf6, 0x37, 0xee, 0x1f, 0x7b, 0xa6, 0x7f, 0x9e, 0xd8, 0xf7, 0x13, 0x6e, 0xe2, 0x43, 0x69, 0xd3, - 0x4d, 0xe7, 0x73, 0x34, 0x80, 0x3d, 0x1b, 0xb2, 0x8d, 0xeb, 0x70, 0x7f, 0x61, 0xb5, 0x55, 0xed, - 0xd4, 0x52, 0xd7, 0xb4, 0x3f, 0x20, 0xbc, 0x71, 0xac, 0xd4, 0xe8, 0x25, 0x17, 0xc3, 0x33, 0x6b, - 0xc8, 0x5d, 0xfc, 0xbf, 0xb1, 0x94, 0x8d, 0x78, 0xf6, 0x16, 0x14, 0x7f, 0x46, 0xff, 0x39, 0xd1, - 0x51, 0x64, 0x17, 0x6f, 0x31, 0x3a, 0xa2, 0x32, 0xe7, 0xba, 0xc4, 0x56, 0x00, 0xdb, 0x2c, 0x65, - 0x0f, 0x26, 0xf8, 0x76, 0xae, 0x64, 0xce, 0xa5, 0xd5, 0xd4, 0xf2, 0x7e, 0x09, 0x57, 0x01, 0x26, - 0x37, 0x47, 0x6e, 0x41, 0x5b, 0xe2, 0xa0, 0x47, 0x0d, 0x7f, 0xca, 0xa5, 0x1a, 0x17, 0x89, 0xfb, - 0x45, 0x01, 0x19, 0x82, 0xd4, 0x35, 0xe4, 0x39, 0x0e, 0x8c, 0xe5, 0x93, 0xcc, 0x88, 0x0b, 0xfe, - 0x97, 0x47, 0xb3, 0x5e, 0x18, 0x9c, 0x88, 0x0b, 0xde, 0x7b, 0x71, 0x39, 0x8f, 0xd0, 0xd5, 0x3c, - 0x42, 0xdf, 0xe7, 0x11, 0xfa, 0xb8, 0x88, 0x2a, 0x57, 0x8b, 0xa8, 0xf2, 0x75, 0x11, 0x55, 0xde, - 0x1c, 0xdc, 0xf0, 0xf2, 0x5f, 0xd9, 0xde, 0x88, 0x32, 0x53, 0x36, 0xc9, 0xac, 0x7b, 0x90, 0xbc, - 0xfb, 0xf5, 0xef, 0x82, 0x3b, 0x5b, 0x85, 0x7e, 0xff, 0x67, 0x00, 0x00, 0x00, 0xff, 0xff, 0xb4, - 0x08, 0x2b, 0xaa, 0xdc, 0x03, 0x00, 0x00, + // 550 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x94, 0xcf, 0x6e, 0x13, 0x3f, + 0x10, 0xc7, 0xe3, 0xfc, 0x69, 0xbb, 0xee, 0xef, 0xd7, 0x22, 0xd3, 0xc3, 0xa6, 0x48, 0x9b, 0x28, + 0x48, 0x34, 0x97, 0xee, 0x2a, 0xb4, 0x12, 0x12, 0x07, 0x24, 0x02, 0x42, 0xaa, 0x90, 0x68, 0xb5, + 0xad, 0xc4, 0x9f, 0xcb, 0xca, 0xbb, 0x71, 0x52, 0xab, 0x89, 0x27, 0xb2, 0x9d, 0x00, 0x7d, 0x06, + 0x0e, 0x3c, 0x02, 0x4f, 0xc1, 0x33, 0xf4, 0xd8, 0x23, 0xe2, 0x50, 0x50, 0x72, 0xe1, 0x31, 0xd0, + 0xda, 0xde, 0xa5, 0x1c, 0x90, 0xa0, 0xa7, 0x78, 0xbe, 0xf3, 0xf1, 0xe4, 0x3b, 0xe3, 0xd1, 0xe2, + 0x1d, 0x50, 0x13, 0x50, 0x5c, 0x45, 0x53, 0x09, 0x1a, 0x24, 0x9b, 0x47, 0xf3, 0x5e, 0xca, 0x34, + 0xed, 0x95, 0x42, 0x68, 0x0e, 0xc4, 0x77, 0x60, 0x58, 0xea, 0x0e, 0xdc, 0x6e, 0x66, 0x26, 0x95, + 0x98, 0x44, 0x64, 0x03, 0x4b, 0x6d, 0x6f, 0x8d, 0x60, 0x04, 0x56, 0xcf, 0x4f, 0x4e, 0x0d, 0x2c, + 0x13, 0xa5, 0x54, 0xb1, 0xf2, 0xef, 0x32, 0xe0, 0xc2, 0xe6, 0x3b, 0xdf, 0x10, 0x26, 0x27, 0x70, + 0xc6, 0xc4, 0x11, 0xe5, 0xf2, 0xb1, 0x4c, 0x63, 0x98, 0x69, 0xa6, 0xc8, 0x23, 0x8c, 0xa9, 0x4c, + 0x13, 0x69, 0x22, 0x1f, 0xb5, 0x6b, 0xdd, 0xf5, 0xfb, 0xad, 0xf0, 0x4f, 0xb6, 0x42, 0x73, 0x2b, + 0xf6, 0x68, 0x79, 0xbf, 0x89, 0xd7, 0x74, 0x5e, 0x35, 0xe1, 0xc2, 0xaf, 0xb6, 0x51, 0xd7, 0x8b, + 0x57, 0x4d, 0x7c, 0x20, 0xc8, 0x1d, 0xec, 0xd9, 0x14, 0xcc, 0xb4, 0x5f, 0x33, 0x39, 0xcb, 0x1e, + 0xce, 0x34, 0x79, 0x8e, 0x3d, 0xa5, 0xd9, 0x34, 0x51, 0xfc, 0x9c, 0xf9, 0xf5, 0x3c, 0xd9, 0x0f, + 0x2f, 0xae, 0x5a, 0xe8, 0xeb, 0x55, 0xeb, 0xde, 0x88, 0xeb, 0xd3, 0x59, 0x1a, 0x66, 0x30, 0x71, + 0x8d, 0xbb, 0x9f, 0x5d, 0x35, 0x38, 0x8b, 0xf4, 0xfb, 0x29, 0x53, 0xe1, 0x81, 0xd0, 0xf1, 0x5a, + 0x5e, 0xe0, 0x98, 0x9f, 0xb3, 0x87, 0xf5, 0x1f, 0x9f, 0x5a, 0xa8, 0xf3, 0x0c, 0x37, 0x8c, 0x29, + 0xf2, 0x00, 0xaf, 0x68, 0x49, 0x07, 0x7f, 0xd3, 0xcf, 0x49, 0xce, 0xc5, 0x0e, 0x77, 0x75, 0x5e, + 0xe3, 0x86, 0x91, 0x09, 0xc1, 0xf5, 0x29, 0xc0, 0xd8, 0x47, 0x6d, 0xd4, 0xad, 0xc7, 0xe6, 0x7c, + 0xd3, 0x7e, 0x5d, 0xe9, 0xcf, 0x08, 0x6f, 0x1a, 0x8f, 0xc7, 0x9a, 0x6a, 0xae, 0x34, 0xcf, 0x14, + 0xd9, 0xc3, 0xab, 0x53, 0x09, 0x43, 0xae, 0x0b, 0xbb, 0xcd, 0xd0, 0x3d, 0x77, 0xfe, 0x94, 0xa5, + 0xd3, 0x27, 0xc0, 0x45, 0x5c, 0x90, 0xe4, 0x15, 0xbe, 0x25, 0x66, 0x93, 0x94, 0xc9, 0x04, 0x86, + 0x89, 0x6b, 0xb6, 0x5a, 0x4e, 0xb1, 0xf2, 0x0f, 0x53, 0xdc, 0xb0, 0x75, 0x0e, 0x87, 0xa6, 0x67, + 0x45, 0xb6, 0x70, 0xc3, 0x2c, 0x83, 0x5f, 0x6b, 0xd7, 0xba, 0xf5, 0xd8, 0x06, 0x9d, 0x0f, 0x08, + 0xaf, 0x1f, 0x01, 0x8c, 0x5f, 0x32, 0x3e, 0x3a, 0xd5, 0x8a, 0xdc, 0xc5, 0xff, 0x2b, 0x4d, 0xd3, + 0x31, 0x4b, 0xde, 0x1a, 0xc5, 0xcd, 0xe8, 0x3f, 0x2b, 0x5a, 0x8a, 0xec, 0xe0, 0xcd, 0x94, 0x8e, + 0xa9, 0xc8, 0x98, 0x2c, 0xb0, 0xaa, 0xc1, 0x36, 0x0a, 0xd9, 0x81, 0x11, 0xbe, 0x9d, 0x81, 0xc8, + 0x98, 0xd0, 0x92, 0x6a, 0x36, 0x28, 0xe0, 0x9a, 0x81, 0xc9, 0xf5, 0x94, 0xbd, 0xd0, 0x11, 0xd8, + 0xeb, 0x53, 0xc5, 0x9e, 0x32, 0x01, 0x93, 0xdc, 0xf1, 0x20, 0x3f, 0x18, 0x0f, 0x5e, 0x6c, 0x83, + 0xdf, 0x17, 0xec, 0x66, 0xa3, 0x29, 0x17, 0xac, 0xff, 0xe2, 0x62, 0x11, 0xa0, 0xcb, 0x45, 0x80, + 0xbe, 0x2f, 0x02, 0xf4, 0x71, 0x19, 0x54, 0x2e, 0x97, 0x41, 0xe5, 0xcb, 0x32, 0xa8, 0xbc, 0xd9, + 0xbf, 0x56, 0xcb, 0x6d, 0xd9, 0xee, 0x98, 0xa6, 0xaa, 0x08, 0xa2, 0x79, 0x6f, 0x3f, 0x7a, 0xf7, + 0xeb, 0x43, 0x60, 0xaa, 0xa7, 0x2b, 0x26, 0xde, 0xfb, 0x19, 0x00, 0x00, 0xff, 0xff, 0x0a, 0xe6, + 0xad, 0xdc, 0x29, 0x04, 0x00, 0x00, } func (this *TokenPairArbRoutes) Equal(that interface{}) bool { @@ -464,6 +468,13 @@ func (this *TokenPairArbRoutes) Equal(that interface{}) bool { if this.TokenOut != that1.TokenOut { return false } + if that1.StepSize == nil { + if this.StepSize != nil { + return false + } + } else if !this.StepSize.Equal(*that1.StepSize) { + return false + } return true } func (this *Route) Equal(that interface{}) bool { @@ -545,6 +556,18 @@ func (m *TokenPairArbRoutes) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.StepSize != nil { + { + size := m.StepSize.Size() + i -= size + if _, err := m.StepSize.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintProtorev(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x22 + } if len(m.TokenOut) > 0 { i -= len(m.TokenOut) copy(dAtA[i:], m.TokenOut) @@ -829,6 +852,10 @@ func (m *TokenPairArbRoutes) Size() (n int) { if l > 0 { n += 1 + l + sovProtorev(uint64(l)) } + if m.StepSize != nil { + l = m.StepSize.Size() + n += 1 + l + sovProtorev(uint64(l)) + } return n } @@ -1057,6 +1084,42 @@ func (m *TokenPairArbRoutes) Unmarshal(dAtA []byte) error { } m.TokenOut = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field StepSize", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProtorev + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthProtorev + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthProtorev + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var v github_com_cosmos_cosmos_sdk_types.Int + m.StepSize = &v + if err := m.StepSize.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipProtorev(dAtA[iNdEx:]) diff --git a/x/protorev/types/token_pair_arb_routes.go b/x/protorev/types/token_pair_arb_routes.go index e796c1c6cd1..2ea6dc8333b 100644 --- a/x/protorev/types/token_pair_arb_routes.go +++ b/x/protorev/types/token_pair_arb_routes.go @@ -2,6 +2,8 @@ package types import ( "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" ) // Creates a new TokenPairArbRoutes object @@ -19,6 +21,10 @@ func (tp *TokenPairArbRoutes) Validate() error { return fmt.Errorf("token names cannot be empty") } + if tp.StepSize == nil || tp.StepSize.LT(sdk.OneInt()) { + return fmt.Errorf("step size must be greater than 0") + } + // The list cannot be nil if tp.ArbRoutes == nil { return fmt.Errorf("the list of routes cannot be nil")