MOOSE: choosing swap routes during execution

Background (simulation vs execution prices)

When a trader asks a router for the best price, the relevant price is the one available when the trade executes. The router usually has to choose which AMMs to route the trade to earlier: it simulates the available routes, divides the order among them, and puts that allocation in a transaction for the user to sign. By the time the transaction reaches the chain, the market may have moved enough to favor a different split.

Consider two routes that offer 1.01 and 1.00 buy tokens per sell token. A router comparing those prices would choose the first. Suppose its rate then falls to 0.98, while the second stays at 1.00. A user who accepted a minimum rate of 0.97 still gets a successful transaction, but receives 0.02 less per unit than the second route could have provided. Tightening the minimum above 0.98 would cause the transaction to revert. To obtain the better price, something has to reconsider the allocation.

These changes can arise from the ordinary operation of a market. Another trade can alter a pool’s reserves, and a professional market maker may adjust its prices as inventory, reference markets, or risk limits change. Proprietary AMMs make this especially relevant because their pricing models can respond to information beyond the reserves of a single pool. The price seen in a simulation reflects the policy at that moment; an order arriving later may face a different one.

While other aggregators will typically simulate the routes on a server, compare them and lock in the route at the time the user signs the swap transaction, MOOSE was built to make this comparison during execution. Its name stands for Marginal Rate Onchain Optimization during Swap Execution. A backend supplies candidate routes and estimates of their liquidity, and the contract uses trades within the transaction to revise how the order is divided. The user specifies a minimum for the completed order, giving the contract room to respond to what it encounters along the way.

Moving the decision into the transaction’s execution brings the information closer to the trade; there is no longer the potential for simulated routes to differ from executed routes. It also makes the allocation problem more immediate: after observing a route’s current price, the contract still has to decide how much of the order that price justifies sending there. While MOOSE has an advantage compared to other aggregators due to allocating volume based on the “live” state of these routes rather than “stale” simulation results, it also has the unique challenge of needing to then allocate that volume onchain in a way that leads to optimal or near-optimal outcomes for users regardless of how market conditions may have changed and with significantly less access to “compute” than a typical backend-based allocation approach would have.

Marginal Rate vs Total Utility

A favorable price for a small trade tells us little about how much volume a route should receive. Suppose one route has an output curve F1(x) = 2x − 0.1x2, while another offers F2(x) = 1.2x. The first starts at a better rate, but its rate deteriorates as more input arrives. For an order of ten units, sending everything to the first route returns ten; sending everything to the second returns twelve.

We can improve on both by asking where each additional unit earns more. The first route’s marginal rate is 2 − 0.2x, so it remains the better destination until it has received four units. At that point, its marginal rate reaches 1.2, the rate offered by the second route. Sending the remaining six units to the second route produces a combined output of 13.6. The benefit comes from using the first route’s better prices while they last.

The example turns on the marginal rate, the output earned by the next small unit of input. If Fi(x) is the output from putting x units through route i, then the marginal rate is its slope:

mi(x) = F′i(x).

For a total input Q and route capacities ci, we want to

maximize ∑i Fi(xi)
subject to ∑i xi = Q and 0 ≤ xi ≤ ci.

Assume that the routes are independent and their output curves are increasing, differentiable, and concave. Concavity means that another unit of input earns no more than the previous one. Under these conditions, moving a small amount from a route with a lower marginal rate to one with a higher marginal rate improves the result, provided the receiving route has room. At the best split, the routes receiving input below capacity therefore end at a common marginal rate, λ. Routes that begin below this rate remain unused, while a route at capacity may still offer a higher rate because we cannot send it any more input.

Concavity also explains why this local comparison is enough. Once the marginal rates are equal, additional input sent to a route earns at most that common rate, while input withdrawn from another gives up at least as much. There is no gain from a larger transfer that the small transfer failed to reveal.

The contract finds this split by water filling. It proposes a common rate and assigns input to each route until that route reaches the rate or its capacity. Lowering the proposed rate assigns more input; raising it assigns less. Searching for the rate at which the allocations add up to the order gives the desired split. The same reasoning applies whether a route uses one pool, an order book, or several pools along a path, as long as its output curve satisfies the assumptions.

By allocating volume in a sequence based on the best available marginal rates rather than the best expected total output per unit of input volume (i.e., average realized rate), MOOSE maximizes its odds of correctly allocating the optimal volume. This is extremely important due to the limitation of partial reverts in the EVM: we cannot partially revert a swap without also reverting all of the swaps that have happened since. An allocation sequencing strategy based on expected average realized rates tends to allocate more volume earlier; this is because the only case in which the best marginal rate route is not the best average rate route is when it does not have the deepest liquidity. This results in an allocation sequence based on the best average rate route being more susceptible to an overallocation error and the ensuing suboptimal swap result should the route with the best marginal rate have deeper liquidity than expected, which would not be discovered until it is too late to roll back the over-allocated route. Conversely, the downside of an allocation sequencing strategy based on the best marginal rate route is simply using more gas due to requiring more total swaps. As discussed in more detail below, when is only enough gas left for a final swap, the allocation uses the best average rate route to maximize the swapper’s total utility rather than the best marginal rate route since there are no longer the resources or desire to revert an earlier swap.

Learning the curves as the order executes

The calculation assumes we know the output curves. In the opening example, the difficulty was precisely that an earlier estimate had become unreliable. MOOSE therefore uses the backend’s curves as a starting point for a decision that will be revised as observations arrive.

The backend can search for candidate routes before the user submits the transaction and pass those candidates to the contract. Each comes with an estimated output curve and a capacity limit. That gives the contract a way to compare how rates would change as input is added, while leaving its execution budget available for measuring the selected candidates. Measurements can revise the curves during execution, and the supplied capacity limits continue to apply.

Before relying on those estimates, the contract tests each route with a small trade, or probe. Each probe runs through the actual execution calls and reports its output through a deliberate revert. That undoes the probe’s swaps while allowing the calling contract to retain the measurement. In the opening example, this is where the first route’s lower rate would become visible. Because the probe and the subsequent swaps occur within one transaction, another transaction cannot intervene between them.

The probe size is five percent of the order, rounded down and limited by the route’s capacity. Using a fraction of the order makes the measurement relevant to the trade being considered. Its size is a practical choice: a larger probe samples more volume, while a smaller one measures closer to the starting rate. The measured average becomes the new estimate of that starting rate. One observation gives limited information about the rest of the curve, so the backend’s slope and curvature still guide the first allocation. Routes whose probes fail or produce no positive rate are removed from consideration.

With these observations, the contract can make its first allocation. It divides the remaining input across the current curves, then chooses the allocated route with the highest starting marginal rate and attempts the amount assigned to it. That amount is a tranche. Using the full allocation carries out what the model currently recommends for that route; splitting it into smaller calls would ordinarily add execution cost to the same plan. After the attempt, the contract revises its estimates and allocates the remaining input again.

A trade also changes the market it is measuring. The average rate over a completed tranche may look attractive even though the last part received much less, and it is that later rate that matters for the next allocation. MOOSE therefore executes a tranche as a body followed by a tail, with the tail limited to the probe size. The tail’s average rate estimates what the route offers after the body has traded. This lets each attempt both execute part of the order and inform the next decision.

The same distinction that motivated MOOSE now appears within the transaction. A route can deliver acceptable output while another use of the input would have delivered more. Before attempting a tranche, the contract therefore estimates the result of leaving roughly ninety percent on the selected route and moving ten percent to the others. That alternative gives the selected route an output threshold to meet.

The other routes already have allocations in the current plan. We need to know what they would return on top of those allocations, so the contract adds the shifted input to their combined amount and recalculates their split. For a tranche of size t, let s = ⌈t/10⌉ be the amount shifted, rounded up to at least ten percent. If aj and bj are the other routes’ allocations before and after this change, the threshold is

Xi = Fi(t − s) + max{0, ∑j≠i[Fj(bj) − Fj(aj)]}.

The first term values the input left on the selected route; the second values the extra output from moving the rest. The contract caps the threshold at the modeled output of the full tranche to account for rounding differences. The comparison requires enough spare capacity on the other routes. When they cannot absorb the shift, the threshold for this attempt is zero and the user’s minimum for the whole order governs settlement. Ten percent is a tuning choice that determines how much input this comparison reconsiders.

If the attempted tranche falls below the threshold, its swaps are undone and its input goes back into the next allocation. The measurements survive, so even this rejected attempt improves the information available for choosing what to try next. The contract first considers other allocated routes while the same amount remains to be sold, giving an alternative a chance before returning to the rejected route.

Updating a curve after an attempt requires accounting for whether the swaps took effect. An accepted tranche leaves the route in a new state, so the observed tail rate becomes its next starting rate and the input sold reduces its remaining capacity. After a rejection, we want a better description of the state to which the route returned. For a declining rate, if the attempt’s average was a and its tail rate was u, a straight marginal curve from 2a − u to u has average a. Fitting that line preserves the measured total output before rounding and gives the allocator a basis for trying a different amount.

A venue that reverts before returning measurements leaves less information to work with. The contract knows the size it attempted, so it narrows its next attempt using the smallest failed input and the largest input known to execute. Limiting the next attempt to their midpoint gives a way to search for an executable size while keeping the unsold input available to all routes.

The cost of another observation

Each of these measurements can improve an allocation, but each also consumes gas. There is a point at which spending more on the decision can outweigh what a better decision is likely to recover. This is why the implementation places bounds on both the number of attempts and the gas available for making them.

For n routes, we give each route an ordinary budget of 5 + n attempts, including its initial probe. We chose that budget through iterative testing. The marginal rate argument supplies an allocation target; it does not prescribe how many observations are worth buying to approach it. The budget balances the opportunity to learn against execution cost, and a different mix of venues or gas costs may favor another value. At most two further attempts are allowed, each smaller than the immediately preceding failed attempt, giving a route a limited opportunity to recover by reducing size.

An attempt count alone cannot account for the different costs of calling different venues. MOOSE also reserves enough gas for one final route attempt plus 180,000 gas for settlement, and limits planning and ordinary calls to the gas outside that reserve. Because the route best suited to the remainder can change after an attempt, the route allowance uses the largest estimated cost among the candidates still available.

When another ordinary attempt would leave too little gas for completion, or planning uses up its allowance, the contract tries to sell the entire remainder through one eligible route. With only one call left, the relevant comparison is how much each route returns on the whole remaining amount. The contract therefore selects the route with the highest estimated total output among those that have capacity and satisfy its attempt and failure limits. This final attempt uses a local threshold of zero and is judged against the user’s minimum for the completed order. If no route can complete it, the transaction reverts.

The user’s minimum gives this process a fixed condition for success. The contract can accept different rates on different tranches, provided their combined output is sufficient. At settlement, it checks that all of the input was sold, that the recorded amounts agree with changes in token balances, and that the user receives at least the agreed minimum after any fee. If those conditions fail, every swap in the transaction is undone. This ability to reverse the whole trade is what allows the allocation to adapt without asking the user to accept an incomplete order.

The quality of the result still depends on the routes and estimates supplied to the contract. Routes that share pools can affect each other’s curves, and discrete token units introduce rounding into an argument originally stated for divisible input. The backend must account for these interactions and provide suitable curves; the contract accepts their shape without checking concavity. Measurements help refine those estimates within the time and gas available.

Evaluating MOOSE therefore means measuring the output recovered by revising the allocation alongside the cost of obtaining that information. Stable routes may leave little to recover, while routes whose prices change before execution create more reason to reconsider an earlier plan. Comparisons across those conditions would help establish when another probe or another attempt earns its cost, and how the practical settings should change with the liquidity being traded.

Execution when the model is wrong

The concavity and independence assumptions give us a useful description of the optimal allocation, but if we were only concerned with the optimal allocation then there are more direct and more efficient allocation mechanisms that could have been chosen. MOOSE’s extra complexity and redundancy is intentional: it was designed to pursue nearly optimal execution even when the market departs from MOOSE’s model or its assumptions. This requires the contract to remain responsive to what a trade actually returns as it decides where to send the rest of the order.

CLOBs such as Kuru and perpl illustrate the difficulty of describing liquidity with a simple curve. Depth is distributed across discrete price levels and can be concentrated in some parts of the book and sparse in others. A book can remain concave while exhibiting abrupt changes in its marginal rate that a smooth curve fits poorly. Allocating from that fit in one or a few swaps saves gas, but can send too much volume past an attractive price level and produce a substantially worse result than a different allocation would have delivered.

MOOSE’s relatively large number of individual swaps gives it repeated opportunities to correct these mistakes while input remains available. If an attempted tranche falls short of the comparison with other routes, the contract can undo it, revise its estimate, and try a different allocation. If a route offers better rates than expected, those observations can justify assigning it more of the remaining input. The extra calls give the contract a way to correct the allocation while the order is still being executed, even when a simple curve continues to describe the liquidity poorly.

How closely this process approaches the best available result when the assumptions fail is an empirical question. It depends on the liquidity encountered, the candidates available, and the gas spent exploring them. The relevant test is how much execution quality MOOSE recovers under those conditions for the additional work it performs.

As liquidity provision becomes more automated and strategic, including sources that may behave adversarially toward a router, this ability to revise the allocation becomes more valuable. A routing mechanism needs to approach the best available result both when its estimates hold and when execution reveals that they were misleading. MOOSE’s additional swaps are a deliberate cost of pursuing not just optimal outcomes when the model and assumptions hold but also near-optimal outcomes when the model breaks and assumptions are invalidated. In other words, it’s @0xtrojan 's fault that the swaps use so much gas :grin:

6 Likes

I guess something that makes this “lots of trades” approach work is that you start mostly interacting with warm slots and warm contracts, which considerably cuts the costs down for subsequent swap attempts.

1 Like

That is exactly correct. It still uses a huge amount of gas, but the vast majority of it is hitting warm slots. We also take steps to try and avoid bringing storage slots to a null value at the end of a transaction; for example, we purposely leave 1 wei of each token type in the address that does the swaps and intermediate steps for multi-hop routes so that we don’t have to initialize those slots again on any subsequent swaps.

1 Like