ADR-029: The executor command grammar — axes over an open ledger, with a runtime-matrix source of truth
Status: accepted.
Implementation: landed (epic
463V2C, closed). See executor-command-grammar.md (the architecture) and executor-grammar-usage.md (the usage guide) for the realized model. This ADR is the decision record.
The degenbot-executor command grammar is the model that turns a solver result
into the bytes a searcher's cmd_executor contract executes (ADR-025 — the
ExecutionStrategy seam's default adapter). Today the grammar is keyed by a
single axis — the hop-protocol tuple (V2|V3|V4)^n — with 35 hand-written
per-family adapters. Every other concern — where the stream's capital comes
from, where its profit goes, which hop wraps which unlock/callback, and the
delta/orderings that make a stream valid — is implicit and hand-written into
each adapter.
That single axis failed to express the invariants, and the runtime harness (caught after byte-parity reified the bugs) proved it:
v2_v2_v4/v2_v4_v4encodedV4_TAKE_COMPACT(WETH, …)before any swap created a positive PoolManager WETH delta, so real v4-core rejected the take (require(cur > 0)→"D0"). Fixed by self-funding the leading V2 hop (e7182b95) — the funding source was wrong for that shape.- The terminal exact-out
V2_SWAP_COMPACT/DIRECT1-weiUniswapV2: Kover-draw class (path-182449;2PT5HH) is the same disease: the terminal V2 hop's funding (an exact-draw against a solver amount) is not an encoded invariant.
Byte-parity is self-referential — it derives expected bytes from the same
enc_* primitives the composer uses — so it cannot see ordering defects.
Separately, the searcher will add non-Uniswap DEX/lender families after the
Uniswap-only variations are proven correct: Balancer (a single external
Vault that holds all tokens and executes swaps), Aave (an external
lender), then Curve. Each needs new cmd_executor primitives. The grammar
must not be shaped so that these force a rewrite.
A family is no longer (V2|V3|V4)^n; it is a shape class over independent
dimensions:
- hop-protocol sequence (
V2|V3|V4|…with swap direction per hop), - funding source — the declared origin of the stream's entry (seed) capital; one per stream, chosen at runtime by the strategy/operator as an economic knob (self-fund = cheaper gas for small opportunities; flash = access to outside capital for large ones). Values: self-funded, pool flash-loan (a flash source pool, in-path or off-path), PoolManager free take, external-lender flash (Aave), ERC-6909 burn-to-settle.
- profit capture — the declared destination of the stream's terminal
profit (the excess over the entry capital refunded); one per stream.
Values: custody, owner (
OWNER_ADDR), native, ERC-6909 mint, and (with the Balancer integration) the Balancer Vault. Modeled values are declared even where the current executor cannot yet express them. - builder bribe — a separate output axis (recipient + amount), paid
via
execute'sconfigparameter; orthogonal to profit capture and needed to incentivize builder inclusion on mainnet.
The inter-hop amounts are an implementation detail — they must be sized correctly to achieve optimum swaps and avoid reversion, but they are not a user-facing axis.
Operations read/write an accounting target — the ledger: the executor's ERC-20 balance, the PoolManager delta, an ERC-6909 held balance, a direct pool-to-pool handoff, or (with Balancer/Aave) an external Vault/lender. The ledger is an interface-defined location, with the current four as concrete instances — never a closed enum — so a Balancer Vault or Aave lender is one more ledger, not a new grammar shape. The ordering invariant the grammar enforces is credit-before-debit within a ledger.
A flash source pool may be in-path (also a hop — the unified borrow-and-swap callback) or off-path (an independent borrowing point). The repayment pivot — the derived hop or mechanism that settles a borrowed ledger — is chosen by token roles + hop coupling, never hand-picked.
Which hop wraps which unlock/callback is a consequence of the ordering
required to satisfy each contract's invariants (ERC-20 funding, transfer
allowances, callbacks, ledger credit) — not a user-facing axis. It is computed
by the grammar, so the author cannot hand-reason it into the D0-style bug.
A protocol's coupling and ledger facts (which ledgers a hop touches,
credit/debit, direction rules) are declarative data so the ledger validator
can prove ordering generically and exhaustively over every
(protocol × funding × capture × bribe) combination. A protocol's swap and
callback mechanics (Solidity callback return-wiring, a Vault deposit, a
lender flashLoan) are code behind a per-protocol interface, tested in
isolation. This is the choice most testable for the ordering property: a
generic validator over declarative facts makes "bad command streams impossible
to write" testable; fully-codified ordering lives per-protocol where nothing
enforces it generically; fully-declarative mechanics cannot express the
executor's imperative wiring.
What "derived" means here (clarified by 6ZIE5X, mechanism chosen BP7KIR):
the deliverable is a Plan tree — the family's ledger decisions authored
as an execution-ordered, callback-nested tree of declarative facts. Two
consumers derive from the same Plan: the encoder produces bytes
(Plan → Vec<u8>, callback subtrees nested inside their FlashSwap/
V4Unlock steps — the nesting IS execution order); the validator walks
the Plan depth-first (Plan → Vec<LedgerOp>, depth-first = execution order)
and enforces the invariants. One representation, no drift, no reordering, no
per-family trace duplication. Byte emission is still code (per-protocol
mechanics behind the encoder); the decisions/facts are data (the Plan).
This supersedes two mechanisms the 6SRC23 POC ruled out: enc_*-instrumentation
(produces emission-order traces, wrong for callback-nested flashes like
v3_v4_v3's 3-level V3c→V3a→V4_UNLOCK nesting) and byte→LedgerOp decoding
(couples the gate to byte layout and fights additivity). The 6YUNQN spike's
ShapeClass+HopFacts byte-derivation of the V2/V3 2-hop slice proved
feasibility of data-driven derivation; a future generic ShapeClass→Plan walker
(the 6ZIE5X (a)-branch, deferred) may build on Plan-authored families, but
per-family Plan authoring is sufficient to deliver D4/D5. What is mandated:
(1) each family's stream is produced by building a Plan then encoding it;
(2) the validator gates the Plan (credit-before-debit, terminal-V2
pre-fund-then-V2_SWAP_CALC, flash-debt-net-zero) for every
(protocol × funding × capture × bribe) combination. Language describing the
emitters as "derived" is to be read as "Plan-derived bytes; ordering proven
by the validator over the Plan," not as data-driven byte synthesis from
ShapeClass.
Correctness is judged by actual execution through the on-chain contract:
the matrix runs the production encoder methods, executes the stream in revm,
and asserts actual_delta == predicted exactly. Byte-encoding is an
implementation detail handled by the encoder methods the matrix calls, so a
future executor revision (new commands, new byte layout) is absorbed by those
methods without re-validating bytes. Byte-parity is demoted to a weak "matches
the current reference" check that the matrix may override (the reference corpus
may itself contain latent bugs). The matrix also validates every produced
Plan through the ledger validator (D4: Plan → LedgerOp depth-first walk,
then LedgerValidator::validate_full) and asserts the ordering invariants hold
— this is the gate that makes credit-before-debit and terminal-V2 pre-fund
unrepresentable, not merely spot-tested.
This epic proves Uniswap-only correctness (matrix bounded at 3-hop + all-V2
any-N) and builds grammar machinery that is hop-count-agnostic by design
(arbitrary N given a valid token path) and admits external ledgers/lenders. The
additive-extensibility proof is modeled via stub external contracts in the
harness — it demonstrates that an external-Vault ledger (Balancer) and an
external lender (Aave) compose as axis values — not a real Balancer/Aave
integration, which is a separate epic including cmd_executor primitive work.
2PT5HH (the 1-wei terminal-V2 fix) is subsumed: the "terminal V2 is
pre-funded-then-V2_SWAP_CALC" rule becomes an encoded ledger invariant fixed
once for all affected families.
Additive, not grafted (clarified by 6ZIE5X): a new protocol composes as
one or more axis values — a new Ledger/FundingSource/ProfitCapture
value + a per-protocol mechanics impl + its own declarative facts (+ new
cmd_executor primitives / LedgerOp variants only where the math is
genuinely different). Some per-protocol complexity is expected and bounded to
that protocol's layer. The additive proof is demonstrated on a
representative row (one new value × one existing protocol) that executes
against a stub and passes the validator — never by grafting the new pool
into every slot of the existing 2-or-3-hop matrix. The combinatorial explosion
of (new protocol × every position × every neighbor × funding × capture) cells
is the bespoke-adapter disease this epic kills; the additive proof must not
rebuild it. Bounded per-protocol complexity in exchange for zero cross-matrix
fan-out is the intended trade.
- Fully-codified ordering (rules inside per-protocol impls): better locality, but ordering validity becomes generically untestable and re-introduces the combinatorial failure mode in trait clothing — rejected.
- Fully-declarative mechanics (express Solidity wiring as data): ordering facts are enumerable, but imperative callback/Vault/lender wiring cannot be data; a data-DSL to fake it is its own bug farm — rejected.
- Fixed-at-deploy axes: defeats the runtime economic funding choice — rejected.
- A new DEX family is one interface impl + one declarative descriptor +
(for genuinely different math) new
cmd_executorprimitives; funding and capture values compose additively instead of multiplying adapters. - The ledger validator and the expanded matrix (family × funding × capture × bribe) become the correctness gate for every future composer/encoder change.
- The
erc6909_profit/use_v4_batchopts, today honored only byv4_v4_v4, become general axis values/ledger strategies honored across families.
Implementation, terminology, and the matrix live under ergo epic 463V2C;
vocabulary is recorded in CONTEXT.md ("Executor command layer"). The runtime
harness is rust/crates/degenbot-simulation/tests/harness_declarative.rs;
grammar is rust/crates/degenbot-executor/src/grammar.rs.