-
Notifications
You must be signed in to change notification settings - Fork 613
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ProtoRev: Route building #3683
ProtoRev: Route building #3683
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left few questions that i have about Routes. Also realized that this module doesnot have a README.md
yet, i think it'll be nice to add one 🌮
x/protorev/keeper/routes.go
Outdated
|
||
var newTrade TradeInfo | ||
for _, trade := range route.Trades { | ||
// 0 is a placeholder for swaps that should be entered into the hot route |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A little confused with Hot Routes, How does the gov update where the hot routes are? Is there an example of what hot route looks like vs Highest liquidity pool route?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIU there would be an admin key for just setting hot routes, and governance can choose who is the hot route admin
x/protorev/keeper/routes.go
Outdated
// BuildOsmoRoute builds a cyclic arbitrage route that starts and ends with osmo given the tokenIn, tokenOut and poolId that were used in the swap | ||
func (k Keeper) BuildOsmoRoute(ctx sdk.Context, tokenIn, tokenOut string, poolId uint64) ([]TradeInfo, error) { | ||
// Creating the first trade in the arb | ||
entryPoolId, err := k.GetOsmoPool(ctx, tokenOut) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this get the highest liquidity osmo pool?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, it gets the highest liquidity osmo pool that has osmo <-> tokenOut as pool assets
inputDenom: "bitcoin", | ||
outputDenom: "ethereum", | ||
poolID: 19, | ||
expected: [][]TestRoute{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Asking to learn: In what case does making trade in "opposite direction" reduce arbitrage? Is there an example available?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I understand, cyclic arbitrage is always going to be favorable in the opposite direction because the pool reserves will essentially let you extract more (relative to the standardized price against other pools and assets) if you decide to swap against the original direction of the trade. You can then take that marginal amount and trade it on pools where prices are not fully in sync.
Not sure if there are exact cases where trading in the opposite direction would reduce arbitrage. @NotJeremyLiu could probably answer that better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@davidterpay is correct.
@stackman27 if a regular user swaps OSMO -> JUNO on a single pool (pool 5 for this example), they are selling OSMO for JUNO, increasing the price of JUNO and decreasing the price of OSMO (relative prices) on pool 5.
If you assume other OSMO/JUNO pools or routes are in sync w.r.t prices, the pool the user just swapped on now has cheap OSMO and expensive JUNO. The goal for the cyclic arbitrage is to find JUNO on a different pool that's now "cheap" compared to the JUNO on pool 5. They want to buy JUNO elsewhere, then sell JUNO on Pool 5 where it's expensive.
So when looking at pool 5, an arbitrager will always go the opposite direction as the original swap to capture the mispricing opportunity (swapping JUNO -> OSMO on pool 5).
Hope that makes sense, but ask more questions if not!
x/protorev/keeper/routes_test.go
Outdated
hasRoute: true, | ||
}, | ||
{ | ||
description: "Route exists for swap in Terra and swap out Atom (no mapping pool)", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this description be "Route doesnot exist for swap in Terra and swap out Atom because the pool doesnot exist"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yea you are correct. i'll switch it up to what you suggested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logic LGTM, nice job!
I only consider de-duplicating BuildOsmoRoute & BuildAtomRoute as blocking
x/protorev/keeper/routes.go
Outdated
func (k Keeper) BuildRoutes(ctx sdk.Context, tokenIn, tokenOut string, poolId uint64) [][]TradeInfo { | ||
routes := make([][]TradeInfo, 0) | ||
|
||
// Check hot routes if enabled |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure where the if enabled
part comes from
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thats a typo, will fix
x/protorev/keeper/routes.go
Outdated
} | ||
|
||
// BuildRoutes builds all of the possible arbitrage routes given the given tokenIn, tokenOut and poolId that were used in the swap | ||
func (k Keeper) BuildRoutes(ctx sdk.Context, tokenIn, tokenOut string, poolId uint64) [][]TradeInfo { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we explicitly make a type Route, thats []TradeInfo
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrapped the trades into a Route struct. Updated in the latest commit.
x/protorev/keeper/routes.go
Outdated
// Iterate through all of the routes and build hot routes | ||
routes := make([][]TradeInfo, 0) | ||
for _, route := range tokenPairArbRoutes.ArbRoutes { | ||
newRoute := make([]TradeInfo, 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
newRoute := make([]TradeInfo, 0) | |
newRoute := make([]TradeInfo, 0, len(route.Trades)) |
x/protorev/keeper/routes.go
Outdated
var newTrade TradeInfo | ||
for _, trade := range route.Trades { | ||
// 0 is a placeholder for swaps that should be entered into the hot route | ||
if trade.Pool == 0 { | ||
pool, err := k.GetAndCheckPool(ctx, poolId) | ||
if err != nil { | ||
return [][]TradeInfo{}, err | ||
} | ||
|
||
newTrade = TradeInfo{ | ||
InputDenom: tokenOut, | ||
OutputDenom: tokenIn, | ||
SwapFee: pool.GetSwapFee(ctx), | ||
Pool: pool, | ||
} | ||
} else { | ||
pool, err := k.GetAndCheckPool(ctx, trade.Pool) | ||
if err != nil { | ||
return [][]TradeInfo{}, err | ||
} | ||
|
||
newTrade = TradeInfo{ | ||
InputDenom: trade.TokenIn, | ||
OutputDenom: trade.TokenOut, | ||
SwapFee: pool.GetSwapFee(ctx), | ||
Pool: pool, | ||
} | ||
} | ||
|
||
newRoute = append(newRoute, newTrade) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Annoying code style comment:
Perhaps extract this into a method BuildTradeInfoRoute
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added new BuildTradeInfoHotRoute
and CheckValidHotRoute
helper functions in latest commit.
x/protorev/keeper/routes.go
Outdated
|
||
var newTrade TradeInfo | ||
for _, trade := range route.Trades { | ||
// 0 is a placeholder for swaps that should be entered into the hot route |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIU there would be an admin key for just setting hot routes, and governance can choose who is the hot route admin
x/protorev/keeper/routes.go
Outdated
// Only append if the arbitrage denom is valid | ||
// In denom and out denom must be the same | ||
// In denom must be uosmo or atom | ||
// There must be exactly three hops in the route |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// There must be exactly three hops in the route | |
// There must be exactly three hops in the route | |
// Note: The hot route itself may comprise of more hops |
x/protorev/keeper/routes.go
Outdated
} | ||
|
||
// BuildAtomRoute builds a cyclic arbitrage route that starts and ends with atom given the tokenIn, tokenOut and poolId that were used in the swap | ||
func (k Keeper) BuildAtomRoute(ctx sdk.Context, tokenIn, tokenOut string, poolId uint64) ([]TradeInfo, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we deduplicate the BuildOsmoRoute BuildAtomRoute logic by making another method that takes in as args
- string for
types.AtomDenomination
vs osmoDenom - Function for k.GetAtomPool vs k.GetOsmoPool
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a new BuildTradeInfoRoute
helper function which abstracts the getter and swap so code is not duplicated.
What is the purpose of the change
This PR implements the logic of building cyclic arbitrage routes given a swap. There are two primary methods for route generation: Highest Liquidity Pools and Hot Routes.
Brief Changelog
Testing and Verifying
This change added tests and can be verified as follows:
Documentation and Release Note
Highest Liquidity Pools
There are two types of trades that must be considered:
x/protorev
will sandwich the pool with either Osmo or Atom on the other end. For example, say that a swap occurs on the Akash ↔ Juno pool. There are four possible routes that can be taken (in order of trades made starting on the left and ending on the right).x/protorev
will look for the opposite asset pools from what was traded in the pool. For example, say a swap has been executed on the Osmo ↔ TokenXYZ pool, tendering OSMO and receiving TokenXYZ, the route generated would be:In both cases, the route that is built will always sandwich the swap that was made. However, we allow for more flexibility in route generation as the highest liquidity method may not be optimal via Hot Routes.
Hot Routes
Populated through the admin account (which will be implemented in a later PR), the module’s keeper holds a KV store that associates token pairs (for example, osmo/juno) to the routes that result in a high percentage of arbitrage profit on Osmosis (as determined by external analysis).
The purpose of storing Hot Routes is a recognition that the Highest Liquidity Pool method may not present the best arbitrage routes. As such, hot routes can be configured through governance to store additional routes that may be more effective at capturing arbitrage opportunities. Each hot route will store a placeholder (the value 0) for where the current swapped pool will fit into the trade. TokenPairArbRoutes are always going to be sorted in the direction of the trade i.e. if we see a swap of Atom -> Osmo we can immediately find a route that goes in the opposite direction by replacing the placeholder (0) with this pool id.