-
Notifications
You must be signed in to change notification settings - Fork 7
Open
Labels
enhancementNew feature or requestNew feature or requestexampleExample code / showcaseExample code / showcase
Description
Motivation
L2 liquidation keepers are a distinct and underserved use case for Ethereum libraries. Unlike L1 MEV (which requires Flashbots bundles and gas auctions), L2 FCFS sequencers reward raw speed — exactly where eth.zig's performance advantage is most tangible.
This example also serves as the reference implementation for perpcity liquidation keepers, demonstrating eth.zig's production readiness on Base.
What the bot does
- Subscribe to
PerpManagercontract logs via WebSocket to maintain an in-memory position state - On each position open/adjust/close event: recompute proximity to liquidation threshold
- Maintain a priority queue of positions sorted by distance from liquidation
- For near-threshold positions: pre-sign the liquidation transaction (ready to broadcast, zero compute on hot path)
- When a position crosses the threshold: broadcast the pre-signed tx directly to the Base sequencer RPC
Why L2 is different
On L1, liquidation bots compete via gas price auctions. On Base (FCFS), the winner is whoever reaches the sequencer first. This means:
- Pre-signing txs (so broadcast is a single syscall) matters
- WebSocket latency to the sequencer RPC matters
- Computation on the hot path (between threshold detection and broadcast) must be zero
eth.zig is uniquely positioned here: comptime ABI decoding, minimal allocations, and direct WebSocket transport with no JS runtime overhead.
Implementation outline
examples/10_liquidation_keeper.zig
// Pre-computed event topic at compile time
const POSITION_OPENED = comptime eth.keccak.eventTopic(
"PositionOpened(address,bytes32,uint256,int256,int256)"
);
// Priority queue: positions sorted by (current_margin_ratio - liq_threshold)
var at_risk = PriorityQueue(PositionRisk).init(allocator);
// Pre-signed tx cache: position_id -> signed_tx_bytes (ready to broadcast)
var presigned = HashMap([32]u8, []u8).init(allocator);
// Hot path: called when position crosses threshold
fn onLiquidatable(position_id: [32]u8) !void {
const tx = presigned.get(position_id) orelse return error.NotPresigned;
_ = try provider.sendRawTransaction(tx); // single RPC call, pre-signed
}Key eth.zig features showcased
- Comptime event topic hashing for zero-cost log matching
- WebSocket
newHeads+getLogssubscription (viawatchLogs— see watchLogs: block-scoped log subscription helper #36) - In-memory state management with pre-signed transaction cache
- Direct
eth_sendRawTransactionto Base RPC - EIP-1559 transaction construction optimized for Base (low base fee)
Configuration
const Config = struct {
rpc_ws_url: []const u8, // wss://base-mainnet.g.alchemy.com/...
rpc_http_url: []const u8, // for fallback
private_key: [32]u8, // keeper wallet
perp_manager: [20]u8, // PerpManager contract address
presign_buffer_bps: u64 = 50, // pre-sign when within 0.5% of liq threshold
};Prerequisites
- WebSocket auto-reconnect and resilience for production bots #35 (WebSocket reconnect) — keeper must survive connection drops
- watchLogs: block-scoped log subscription helper #36 (watchLogs helper) — for block-scoped event processing with reconnect
Deliverable
examples/10_liquidation_keeper.zig with:
- Works against Anvil fork of Base
- Configurable via environment variables
- README section explaining the L2 FCFS advantage and why pre-signing matters
- Benchmark: time from threshold detection to
sendRawTransactioncall (should be <1ms on Apple Silicon)
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or requestexampleExample code / showcaseExample code / showcase