Optimal DEX swap-path routing across liquidity pools (Uniswap v2/v3-style).
Given an input token, output token, amount, and a candidate pool set, returns the path that maximizes output — direct or multi-hop.
Approach: pure-Elixir analytic routing + on-chain v3 quoting (chosen)
The task scoped three candidate approaches, all of which were rejected:
| Approach | Rejected because |
|---|---|
(a) Rust routing libs via onchain_evm | onchain_evm depends on onchain. Routing here would invert the dependency graph, and onchain's charter is pure Elixir, no native deps. |
| (b) Elixir + revm local simulation | revm is a Rust NIF (lives in onchain_evm). Same dependency inversion + native-dep violation. |
| (c) QuickBEAM + Uniswap v3 SDK | QuickBEAM is a Zig NIF (lives in onchain_js). Same inversion; also ships a full JS runtime to quote a swap. |
All three pull a native dependency onchain cannot take and invert the
dependency graph (onchain_evm/onchain_js build on top of onchain). The
chosen fourth approach keeps routing in the pure-Elixir layer where it belongs:
- Uniswap v2-style pools — output computed analytically from reserves with
the canonical constant-product formula (
amount_out_v2/4). Pure, fast, unit-testable, no RPC. - Uniswap v3-style pools — output obtained from the on-chain QuoterV2
contract via
eth_call(quoteExactInputSingle), which simulates tick crossings exactly. Aneth_callread is squarelyonchain's wheelhouse and avoids a fragile single-tick approximation that would silently mis-rank large trades.
Path enumeration is a pure graph walk over the pool set (tokens as nodes, pools as edges); for a fixed token path the per-hop output-maximizing pool is chosen greedily, which is globally optimal because each pool's output is monotonic in its input.
Limitations
- No split routing. A single best path is returned; the amount is not split across pools (production aggregators do — out of scope for this prototype).
- v3 hops require
opts[:rpc_url](and reach the mainnet QuoterV2 by default; override withopts[:quoter]). v2 hops need no RPC.
Sources
- Uniswap v2
getAmountOut:Uniswap/v2-peripheryUniswapV2Library.sol. - QuoterV2
0x61fFE014bA17989E743c5F6cB21bF9697530B21eandquoteExactInputSingleABI:Uniswap/v3-peripheryIQuoterV2.sol, Uniswap Ethereum deployments docs.
Functions
| Function | Purpose |
|---|---|
route/5 | Optimal path for a token pair across a candidate pool set |
quote_pool/4 | Output of a single pool hop (v2 analytic / v3 on-chain quote) |
amount_out_v2/4 | Pure constant-product output for one v2 hop |
API Functions
| Function | Arity | Description | Param Kinds |
|---|---|---|---|
amount_out_v2 | 4 | Constant-product (Uniswap v2) output for one hop. Pure math. | amount_in: value, reserve_in: value, reserve_out: value, fee_bps: value |
quote_pool | 4 | Quote the output of a single pool hop for a given input token. | pool: value, token_in: value, amount_in: value, opts: value |
route | 5 | Find the output-maximizing swap path across a candidate pool set. | token_in: value, token_out: value, amount_in: value, pools: value, opts: value |
Summary
Functions
Constant-product output for a single Uniswap v2-style hop.
Quote the output of a single pool hop for a given input token.
Find the output-maximizing swap path across a candidate pool set.
Functions
@spec amount_out_v2(integer(), integer(), integer(), non_neg_integer()) :: {:ok, non_neg_integer()} | {:error, term()}
Constant-product output for a single Uniswap v2-style hop.
Implements the canonical getAmountOut:
amount_out = (amount_in * f * reserve_out) / (reserve_in * 10000 + amount_in * f)
where f = 10000 - fee_bps. Source: UniswapV2Library.sol (997/1000 = 0.3%).
Examples
iex> Onchain.DEX.Router.amount_out_v2(100, 1000, 1000, 30)
{:ok, 90}
iex> Onchain.DEX.Router.amount_out_v2(0, 1000, 1000, 30)
{:error, :insufficient_input_amount}
iex> Onchain.DEX.Router.amount_out_v2(100, 0, 1000, 30)
{:error, :insufficient_liquidity}
@spec quote_pool( Onchain.DEX.Router.Pool.t(), String.t() | binary(), non_neg_integer(), keyword() ) :: {:ok, non_neg_integer()} | {:error, term()}
Quote the output of a single pool hop for a given input token.
Parameters
pool- %Onchain.DEX.Router.Pool{} to quote against (value)token_in- Input token of this hop (0x hex or 20-byte binary) (value)amount_in- Raw input amount (integer) (value)opts- Options: :rpc_url (v3), :quoter, :timeout, :block (default:[], value)
Returns
Raw output amount of the swap through this pool ({:ok, non_neg_integer()} | {:error, term()})
# descripex:contract
%{
params: %{
opts: %{
default: [],
description: "Options: :rpc_url (v3), :quoter, :timeout, :block",
kind: :value
},
pool: %{
description: "%Onchain.DEX.Router.Pool{} to quote against",
kind: :value
},
amount_in: %{description: "Raw input amount (integer)", kind: :value},
token_in: %{
description: "Input token of this hop (0x hex or 20-byte binary)",
kind: :value
}
},
returns: %{
type: "{:ok, non_neg_integer()} | {:error, term()}",
description: "Raw output amount of the swap through this pool",
example: "90"
}
}
@spec route( String.t() | binary(), String.t() | binary(), non_neg_integer(), [Onchain.DEX.Router.Pool.t()], keyword() ) :: {:ok, Onchain.DEX.Router.Route.t()} | {:error, term()}
Find the output-maximizing swap path across a candidate pool set.
Parameters
token_in- Input token address (0x hex or 20-byte binary) (value)token_out- Output token address (0x hex or 20-byte binary) (value)amount_in- Raw input amount (integer, token base units) (value)pools- List of %Onchain.DEX.Router.Pool{} candidates to route through (value)opts- Options: :rpc_url (v3 hops), :quoter, :max_hops (default 3), :timeout, :block (default:[], value)
Returns
Optimal route with path, per-hop pools, and output amount ({:ok, %Onchain.DEX.Router.Route{}} | {:error, term()})
# descripex:contract
%{
params: %{
opts: %{
default: [],
description: "Options: :rpc_url (v3 hops), :quoter, :max_hops (default 3), :timeout, :block",
kind: :value
},
pools: %{
description: "List of %Onchain.DEX.Router.Pool{} candidates to route through",
kind: :value
},
amount_in: %{
description: "Raw input amount (integer, token base units)",
kind: :value
},
token_in: %{
description: "Input token address (0x hex or 20-byte binary)",
kind: :value
},
token_out: %{
description: "Output token address (0x hex or 20-byte binary)",
kind: :value
}
},
returns: %{
type: "{:ok, %Onchain.DEX.Router.Route{}} | {:error, term()}",
description: "Optimal route with path, per-hop pools, and output amount",
example: "{:ok, %Onchain.DEX.Router.Route{amount_out: 90}}"
}
}