Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions bench/u256_bench.zig
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,51 @@ fn benchMulDiv() void {
std.mem.doNotOptimizeAway(&result);
}

// DEX V2 getAmountOut with configurable fee (dex/v2.zig)
fn benchDexV2AmountOut() void {
var amount_in: u256 = ONE_ETH;
var reserve_in: u256 = RESERVE_IN;
var reserve_out: u256 = RESERVE_OUT;
std.mem.doNotOptimizeAway(&amount_in);
std.mem.doNotOptimizeAway(&reserve_in);
std.mem.doNotOptimizeAway(&reserve_out);

const amount_out = eth.dex_v2.getAmountOut(amount_in, reserve_in, reserve_out, 997, 1000);
std.mem.doNotOptimizeAway(&amount_out);
}

// DEX V2 multi-hop: 3-hop path
fn benchDexV2MultiHop() void {
const path = [_]eth.dex_v2.Pair{
.{ .reserve_in = RESERVE_IN, .reserve_out = RESERVE_OUT },
.{ .reserve_in = 200_000_000_000, .reserve_out = 50_000_000_000_000_000_000 },
.{ .reserve_in = 50_000_000_000_000_000_000, .reserve_out = 100_000_000_000 },
};
var amount_in: u256 = ONE_ETH;
std.mem.doNotOptimizeAway(&amount_in);

const result = eth.dex_v2.getAmountsOut(amount_in, &path);
std.mem.doNotOptimizeAway(&result);
}

// DEX V3 getSqrtRatioAtTick
fn benchDexV3TickToPrice() void {
var tick: i24 = 10000;
std.mem.doNotOptimizeAway(&tick);
const result = eth.dex_v3.getSqrtRatioAtTick(tick);
std.mem.doNotOptimizeAway(&result);
}

// DEX V3 computeSwapStep
fn benchDexV3SwapStep() void {
var current: u256 = SQRT_PRICE;
var target: u256 = eth.dex_v3.getSqrtRatioAtTick(-100).?;
std.mem.doNotOptimizeAway(&current);
std.mem.doNotOptimizeAway(&target);
const result = eth.dex_v3.computeSwapStep(current, target, 1_000_000_000_000_000_000, 1_000_000, 3000);
std.mem.doNotOptimizeAway(&result);
}

// UniswapV4 getNextSqrtPriceFromAmount0RoundingUp
fn benchUniswapV4Swap() void {
var liquidity: u256 = ONE_ETH;
Expand Down Expand Up @@ -202,6 +247,11 @@ pub fn main() !void {
try runAndPrint("u256_uniswapv2_optimized", benchUniswapV2Optimized, stdout);
try runAndPrint("u256_mulDiv", benchMulDiv, stdout);
try runAndPrint("u256_uniswapv4_swap", benchUniswapV4Swap, stdout);
// DEX benchmarks
try runAndPrint("dex_v2_amount_out", benchDexV2AmountOut, stdout);
try runAndPrint("dex_v2_multi_hop_3", benchDexV2MultiHop, stdout);
try runAndPrint("dex_v3_tick_to_price", benchDexV3TickToPrice, stdout);
try runAndPrint("dex_v3_swap_step", benchDexV3SwapStep, stdout);

try stdout.print("\n", .{});

Expand All @@ -215,6 +265,10 @@ pub fn main() !void {
try runAndJson("u256_uniswapv2_optimized", benchUniswapV2Optimized, stdout);
try runAndJson("u256_mulDiv", benchMulDiv, stdout);
try runAndJson("u256_uniswapv4_swap", benchUniswapV4Swap, stdout);
try runAndJson("dex_v2_amount_out", benchDexV2AmountOut, stdout);
try runAndJson("dex_v2_multi_hop_3", benchDexV2MultiHop, stdout);
try runAndJson("dex_v3_tick_to_price", benchDexV3TickToPrice, stdout);
try runAndJson("dex_v3_swap_step", benchDexV3SwapStep, stdout);

try stdout.flush();
}
86 changes: 86 additions & 0 deletions docs/content/docs/batch-calls.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
title: Batch RPC Calls
description: Send multiple eth_call requests in a single JSON-RPC round-trip for MEV and high-throughput applications.
---

eth.zig supports [JSON-RPC batch requests](https://www.jsonrpc.org/specification#batch), sending multiple `eth_call` requests in a single HTTP POST. This dramatically reduces latency when evaluating many candidates per block.

## Basic Usage

```zig
const eth = @import("eth");

// Set up provider
var transport = eth.http_transport.HttpTransport.init(allocator, "https://rpc.example.com");
defer transport.deinit();
var provider = eth.provider.Provider.init(allocator, &transport);

// Create a batch
var batch = eth.provider.BatchCaller.init(allocator, &provider);
defer batch.deinit();

// Add calls (returns index for result retrieval)
const idx0 = try batch.addCall(pool_a_address, quote_calldata_a);
const idx1 = try batch.addCall(pool_b_address, quote_calldata_b);
const idx2 = try batch.addCall(pool_c_address, quote_calldata_c);

// Execute all in one HTTP request
const results = try batch.execute();
defer eth.provider.freeBatchResults(allocator, results);

// Each result is either .success or .rpc_error
switch (results[idx0]) {
.success => |data| {
// data contains the decoded bytes from the RPC response
// Decode as needed (e.g., ABI decode a uint256)
},
.rpc_error => |err| {
// err.code and err.message describe what went wrong
// (e.g., execution reverted, invalid params)
},
}
```

## How It Works

Under the hood, `BatchCaller` uses the [JSON-RPC batch spec](https://www.jsonrpc.org/specification#batch):
- Each call is formatted as an individual JSON-RPC request with a unique `id`
- All requests are wrapped in a JSON array and sent as a single HTTP POST
- Responses may arrive in any order -- `BatchCaller` matches them by `id` and returns results in the original `addCall` order

## Per-Call Error Handling

Some calls in a batch may succeed while others revert. Each `BatchCallResult` is independent:

```zig
for (results, 0..) |result, i| {
switch (result) {
.success => |data| std.debug.print("Call {d}: {d} bytes\n", .{ i, data.len }),
.rpc_error => |err| std.debug.print("Call {d}: error {d} - {s}\n", .{ i, err.code, err.message }),
}
}
```

## Reusing a Batch

Call `reset()` to clear pending calls and reuse the `BatchCaller`:

```zig
batch.reset();
// Add new calls for the next block...
_ = try batch.addCall(new_target, new_calldata);
const new_results = try batch.execute();
defer eth.provider.freeBatchResults(allocator, new_results);
```

## When to Use Batch vs Multicall

| Feature | BatchCaller | Multicall3 |
|---------|-------------|------------|
| Protocol | JSON-RPC batch | On-chain contract call |
| Atomicity | Independent calls | Single transaction |
| Node support | All JSON-RPC nodes | Requires Multicall3 deployment |
| Gas overhead | None | Contract execution gas |
| Best for | Mixed RPC methods | Same-block state consistency |

For MEV searchers: use `BatchCaller` when you need to query multiple pools across different blocks or need raw `eth_call` flexibility. Use `Multicall3` when you need atomic same-block reads.
185 changes: 185 additions & 0 deletions docs/content/docs/dex-math.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
---
title: DEX Math
description: Pure Zig Uniswap V2/V3 price computation for off-chain MEV simulation.
---

eth.zig includes pure Zig implementations of Uniswap V2 and V3 math, enabling off-chain price computation without RPC calls. This is critical for MEV searchers who need to evaluate hundreds of arb paths per block at sub-microsecond latency.

## Uniswap V2

### Single Swap

```zig
const eth = @import("eth");

// Standard Uniswap V2 (0.3% fee = 997/1000)
const amount_out = eth.dex_v2.getAmountOut(
1_000_000_000_000_000_000, // 1 ETH input
100_000_000_000_000_000_000, // 100 ETH reserve_in
200_000_000_000, // 200k USDC reserve_out
997, 1000, // fee: 0.3%
);

// SushiSwap / PancakeSwap (0.25% fee = 9975/10000)
const sushi_out = eth.dex_v2.getAmountOut(
amount_in, reserve_in, reserve_out,
9975, 10000, // fee: 0.25%
);
```

### Inverse: Required Input

```zig
// How much ETH do I need to get exactly 1000 USDC out?
const required_input = eth.dex_v2.getAmountIn(
1_000_000_000, // 1000 USDC desired output
reserve_eth, reserve_usdc,
997, 1000,
) orelse {
// null = insufficient liquidity (amount_out >= reserve_out)
return error.InsufficientLiquidity;
};
```

### Multi-Hop Paths

```zig
const path = [_]eth.dex_v2.Pair{
.{ .reserve_in = 100e18, .reserve_out = 200_000e6 }, // ETH (18 dec) -> USDC (6 dec)
.{ .reserve_in = 300_000e6, .reserve_out = 50e18 }, // USDC (6 dec) -> DAI (18 dec)
};

// Forward: how much DAI for 1 ETH?
const output = eth.dex_v2.getAmountsOut(1e18, &path);

// Reverse: how much ETH for 10 DAI?
const input = eth.dex_v2.getAmountsIn(10e18, &path);
```

### Arbitrage Profit

```zig
// Circular path: buy on pool A, sell on pool B
const path = [_]eth.dex_v2.Pair{
.{ .reserve_in = 1_000_000, .reserve_out = 2_000_000_000 },
.{ .reserve_in = 2_000_000_000, .reserve_out = 2_000_000 },
};

if (eth.dex_v2.calculateProfit(1000, &path)) |profit| {
// profit = output - input (positive means arb exists)
}
```

## Uniswap V3

### Tick/Price Conversion

```zig
// Convert tick index to sqrtPriceX96 (Q96 fixed-point)
const sqrt_price = eth.dex_v3.getSqrtRatioAtTick(100).?; // tick 100 ~ price 1.01
const sqrt_price_0 = eth.dex_v3.getSqrtRatioAtTick(0).?; // tick 0 = price 1.0 = Q96

// Convert sqrtPriceX96 back to tick
const tick = eth.dex_v3.getTickAtSqrtRatio(sqrt_price).?; // = 100
```

> These functions return `null` for out-of-range ticks (outside `MIN_TICK`..`MAX_TICK`)
> or invalid sqrt prices. Always check the result before unwrapping with `.?`.

### Token Amount Deltas

```zig
const sqrt_a = eth.dex_v3.getSqrtRatioAtTick(0).?;
const sqrt_b = eth.dex_v3.getSqrtRatioAtTick(100).?;
const liquidity: u128 = 1_000_000_000_000_000_000;

// How much token0 for a price move from tick 0 to tick 100?
const token0_amount = eth.dex_v3.getAmount0Delta(sqrt_a, sqrt_b, liquidity, true).?;

// How much token1?
const token1_amount = eth.dex_v3.getAmount1Delta(sqrt_a, sqrt_b, liquidity, true).?;
```

### Swap Step Simulation

```zig
// Simulate a single swap step within one tick range
const step = eth.dex_v3.computeSwapStep(
current_sqrt_price, // current pool price
target_sqrt_price, // next initialized tick boundary
pool_liquidity, // current active liquidity
@as(i256, amount_in), // positive = exact input, negative = exact output
3000, // fee: 0.3% (3000 pips)
);

// step.sqrt_ratio_next_x96 -- price after this step
// step.amount_in -- tokens consumed
// step.amount_out -- tokens produced
// step.fee_amount -- fee charged
```

### Full Multi-Tick Swap Simulation

```zig
const ticks = [_]eth.dex_v3.TickInfo{
.{ .tick = -100, .liquidity_net = 500e18, .sqrt_price_x96 = eth.dex_v3.getSqrtRatioAtTick(-100).? },
.{ .tick = -200, .liquidity_net = 300e18, .sqrt_price_x96 = eth.dex_v3.getSqrtRatioAtTick(-200).? },
};

const result = eth.dex_v3.simulateSwap(
current_sqrt_price,
current_liquidity,
&ticks,
amount_in,
true, // zero_for_one (selling token0)
3000, // 0.3% fee
);

// result.amount_in_consumed -- total input consumed
// result.amount_out -- total output received
// result.sqrt_price_final_x96 -- final pool price
// result.ticks_crossed -- number of tick boundaries crossed
```

## Cross-DEX Router

Route through mixed V2/V3 pools:

```zig
const hops = [_]eth.dex_router.Pool{
.{ .v2 = .{ .reserve_in = 100e18, .reserve_out = 200_000e6 } },
.{ .v3 = .{
.sqrt_price_x96 = current_sqrt,
.liquidity = pool_liquidity,
.ticks = &tick_array,
.fee_pips = 3000,
.zero_for_one = true,
} },
};

const output = eth.dex_router.quoteExactInput(1e18, &hops);
```

### Arbitrage Detection

```zig
if (eth.dex_router.findArbOpportunity(&circular_path, max_input)) |arb| {
// arb.profit -- expected profit
// arb.optimal_input -- optimal trade size (found via binary search)
}
```

## Fee Configurations

| DEX | fee_numerator | fee_denominator | Fee |
|-----|--------------|-----------------|-----|
| Uniswap V2 | 997 | 1000 | 0.30% |
| SushiSwap | 997 | 1000 | 0.30% |
| PancakeSwap | 9975 | 10000 | 0.25% |
| Uniswap V3 | fee_pips=500 | -- | 0.05% |
| Uniswap V3 | fee_pips=3000 | -- | 0.30% |
| Uniswap V3 | fee_pips=10000 | -- | 1.00% |

## Performance

All math uses eth.zig's limb-native u256 arithmetic -- no heap allocation, no LLVM software division routines. Run `zig build bench-u256` to measure on your hardware.
2 changes: 2 additions & 0 deletions docs/content/docs/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"comptime",
"ens",
"websockets",
"batch-calls",
"dex-math",
"---Reference---",
"modules",
"benchmarks",
Expand Down
Loading
Loading