Onchain.DEX.Router (onchain v0.7.0)

Copy Markdown View Source

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:

ApproachRejected because
(a) Rust routing libs via onchain_evmonchain_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 simulationrevm is a Rust NIF (lives in onchain_evm). Same dependency inversion + native-dep violation.
(c) QuickBEAM + Uniswap v3 SDKQuickBEAM 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. An eth_call read is squarely onchain'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 with opts[:quoter]). v2 hops need no RPC.

Sources

  • Uniswap v2 getAmountOut: Uniswap/v2-periphery UniswapV2Library.sol.
  • QuoterV2 0x61fFE014bA17989E743c5F6cB21bF9697530B21e and quoteExactInputSingle ABI: Uniswap/v3-periphery IQuoterV2.sol, Uniswap Ethereum deployments docs.

Functions

FunctionPurpose
route/5Optimal path for a token pair across a candidate pool set
quote_pool/4Output of a single pool hop (v2 analytic / v3 on-chain quote)
amount_out_v2/4Pure constant-product output for one v2 hop

API Functions

FunctionArityDescriptionParam Kinds
amount_out_v24Constant-product (Uniswap v2) output for one hop. Pure math.amount_in: value, reserve_in: value, reserve_out: value, fee_bps: value
quote_pool4Quote the output of a single pool hop for a given input token.pool: value, token_in: value, amount_in: value, opts: value
route5Find 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

amount_out_v2(amount_in, reserve_in, reserve_out, fee_bps)

@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}

quote_pool(pool, token_in, amount_in, opts \\ [])

@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"
  }
}

route(token_in, token_out, amount_in, pools, opts \\ [])

@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}}"
  }
}