Lorenz
Architecture

03. Core Domain Model

lorenz-core, the shared deterministic vocabulary: domain newtypes, the Dex enum, strict typed EngineConfig, the error type, and the Hop / TradeRecord telemetry records.

lorenz-core is the shared vocabulary of the whole platform. Its design rule, from the crate docs:

This crate must stay free of any I/O, RPC client or on-chain dependency. It is the deterministic vocabulary that both the data plane and the control plane agree on, which is what makes traces replayable.

Module map (crates/lorenz-core/src/):

ModuleContents
typesDomain newtypes and the Dex enum
configTyped, strict EngineConfig and sub-configs
errorThe single library error type and Result alias
telemetrytracing bootstrap + Hop / TradeRecord records
libRe-exports error::{Error, Result}

Newtypes (types.rs)

The crate uses newtypes instead of bare u64/String so the compiler prevents mixing, say, a token amount with a basis-point value. Every quantity has a name and a unit.

TypeUnderlyingMeaning & notes
TokenId(String)opaque stringA token mint. Kept as a string so the crate needs no Solana SDK; data-plane crates parse it into a real Pubkey at the edge. From<&str>, Display.
PoolId(String)opaque stringA liquidity pool / pair account id. From<&str>, Display.
Amount(u64)u64A raw token amount in base units (lamports for SOL, smallest unit for SPL). Always integer, no floating-point money. Has Amount::ZERO and as_u64().
Bps(u32)u32Basis points (1 bp = 0.01%). DENOMINATOR = 10_000. as_fraction() -> f64 is for analytics/logging only, never for settlement.

The Dex enum

Identifies which DEX a pool belongs to (#[serde(rename_all = "snake_case")]):

RaydiumAmm | RaydiumClmm | RaydiumCpmm | MeteoraDlmm | MeteoraDamm
| Whirlpool | PumpAmm | Solfi | Vertigo

This list mirrors the integration targets; see chapter 04 / lorenz-dex for the decoder status of each (only Raydium AMM v4, Raydium CPMM, and Whirlpool are decoded today; the rest are NotImplemented).

Why newtypes matter here

Amount and Bps both wrap integers but are different units; the type system refuses to add a fee (Bps) to a balance (Amount). Bps::as_fraction() exists but is documented as analytics-only, reinforcing the determinism principle: the floating-point path is quarantined away from settlement math.

Typed, strict config (config.rs)

The configuration model uses #[serde(deny_unknown_fields)] on every struct, so a typo or unsupported section fails loudly instead of being silently ignored. This is a direct lesson from experience: a prior project silently dropped whole config sections ([jito], [kamino_flashloan]) because the struct never read them.

EngineConfig
├── rpc:   RpcConfig
├── risk:  RiskConfig
└── costs: CostConfig

RpcConfig

FieldTypeNotes
urlStringRead endpoint (account/stream subscriptions).
geyser_urlOption<String>Optional Yellowstone/Geyser gRPC endpoint (#[serde(default)]).

No signing material lives in config types. Private keys/mnemonics are loaded at the process edge and never serialized. This is both a design rule and a security property (see Security).

RiskConfig: the hard ceilings

FieldTypeMeaning
max_positionu64Maximum notional per arbitrage, in base units of the loaned asset. The agent may tighten but never exceed this.
min_profitu64Minimum net profit (after costs) required to fire, in base units.
max_consecutive_lossesu32Consecutive failed/loss trades before the kill-switch trips.

These are the values the control plane treats as ceilings and the on-chain program mirrors (spend cap, profit floor).

CostConfig: the shared cost model inputs

FieldTypeMeaning
flash_loan_feeBpsFlash-loan provider fee.
priority_fee_lamportsu64Priority fee per transaction.
jito_tip_lamportsu64Jito bundle tip.
slippage_per_hopBpsAssumed extra slippage per hop on top of pool math.

CostConfig is consumed by both the backtester (lorenz-backtest::CostModel) and is intended for the live engine, so simulated economics use the same numbers as production accounting (invariant O4).

Parsing

EngineConfig::from_toml(&str) -> Result<EngineConfig> parses TOML and returns a descriptive Error::Config on failure rather than panicking. Tests confirm a valid sample parses and that an unknown [jito] section is rejected.

Error type (error.rs)

A single thiserror-derived enum for the deterministic core, with pub type Result<T> = std::result::Result<T, Error>:

VariantMessage
Config(String)configuration error: …
InvalidPool(String)invalid pool state: …
Overflow(&'static str)arithmetic overflow in …
NoCycleno arbitrage cycle found
RiskViolation(String)risk limit violated: …

Note that lorenz-dex and lorenz-stream define their own local error types (DecodeError, StreamError) rather than overloading this one, keeping each boundary's failure modes precise.

Telemetry & records (telemetry.rs)

This module is the observability bootstrap plus the structured records that make every run replayable and auditable.

  • init_tracing() -> Result<(), SetGlobalDefaultError> initializes a structured tracing subscriber driven by RUST_LOG (defaults to info). Idempotent-friendly: callers in tests can ignore the error if a global subscriber already exists.

  • Hop is one hop of an arbitrage route: pool: PoolId, token_in, token_out, amount_in: Amount, amount_out: Amount.

  • TradeRecord is an immutable record of a decision the engine made, persisted append-only so any run can be reconstructed:

    FieldTypeMeaning
    tsu64Unix timestamp (seconds).
    routeVec<Hop>The hops walked.
    notionalAmountNotional borrowed for the cycle.
    gross_profiti128Gross output minus notional, before fees (signed).
    net_profiti128Net profit after the full cost model (signed).
    submittedboolWhether the engine actually submitted (vs simulated/skipped).

TradeRecord is the up-channel payload of the data/control contract: it is what the backtester emits and what a live telemetry pipeline would feed to the control plane.

Continue to 04: Data Plane.

On this page