Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
b31e6de
docs(audit): design findings 2-20 hardening
1kresh Jul 9, 2026
b9330a9
docs(audit): plan findings 2-20 implementation
1kresh Jul 9, 2026
faec9e9
build(deps): update Go toolchain to 1.26.5
1kresh Jul 9, 2026
1dae64a
chore(3f): regenerate vendored API client
1kresh Jul 9, 2026
f161ff0
ci(codegen): verify deterministic generated output
1kresh Jul 9, 2026
96f0372
fix(codegen): preserve generated drift evidence
1kresh Jul 9, 2026
e343069
feat(http): bound generated client responses
1kresh Jul 9, 2026
aac250c
fix(api): reject oversized generated responses
1kresh Jul 9, 2026
e00e26f
test(rfq): return after nil quote assertion
1kresh Jul 9, 2026
f4499fb
fix(chain): preflight and redact RPC endpoints
1kresh Jul 9, 2026
ca70e42
fix(chain): sanitize non-success RPC responses
1kresh Jul 9, 2026
2d2c15a
fix(config): remove unused generic websocket endpoint
1kresh Jul 9, 2026
aaff4fb
docs(rfq): clarify single-endpoint RPC dialing
1kresh Jul 9, 2026
5a7a356
feat(chain): support pinned-block multicalls
1kresh Jul 9, 2026
47641b2
fix(txmanager): enforce configured fee caps
1kresh Jul 9, 2026
04c82d8
feat(txmanager): configure bounded replacement policy
1kresh Jul 9, 2026
2284178
fix(txmanager): supervise canonical transaction lifecycles
1kresh Jul 9, 2026
bbd480b
fix(txmanager): enforce lifecycle ownership and deadlines
1kresh Jul 10, 2026
ff8fbec
fix(txmanager): poll transaction attempts concurrently
1kresh Jul 10, 2026
257c4d4
fix(solvers): reconcile unresolved transaction outcomes
1kresh Jul 10, 2026
0288b5e
test(signer): characterize production signing paths
1kresh Jul 10, 2026
ee4cf0c
fix(oev): join settlement attribution workers
1kresh Jul 10, 2026
85b4a06
fix(runtime): supervise servers and transaction workers
1kresh Jul 10, 2026
9ed2200
test(signer): cover keystore signing paths
1kresh Jul 10, 2026
1a52914
docs(runtime): document transaction supervision
1kresh Jul 10, 2026
1315d8c
fix(rfq): select backend orders by exact id
1kresh Jul 10, 2026
d334de7
fix(3f): align offer lifetime with discovery
1kresh Jul 10, 2026
bd9d5a5
docs(runtime): clarify replacement signing ownership
1kresh Jul 10, 2026
c0cad51
fix(rfq): reject a zero executor address
1kresh Jul 10, 2026
f81b761
perf(rfq): amortize fill plan eviction
1kresh Jul 10, 2026
27d7eb8
fix(oev): harden websocket transport
1kresh Jul 10, 2026
90331e0
refactor(rfq): name open backend status
1kresh Jul 10, 2026
6b4009a
fix(rfq): fail closed on unknown adapter pause state
1kresh Jul 10, 2026
879debd
fix(rfq): bind fills to the signed order
1kresh Jul 10, 2026
e46ea09
fix(3f): preserve exact auction signature fields
1kresh Jul 10, 2026
6453197
fix(oev): track component freshness independently
1kresh Jul 10, 2026
40f558a
fix(oev): deduplicate liquidation results
1kresh Jul 10, 2026
78dbf4a
fix(rfq): accept omitted output projections
1kresh Jul 10, 2026
0f5b366
test(rfq): pin authorization call layout
1kresh Jul 10, 2026
0b7922c
fix(oev): read coherent Morpho accrual state
1kresh Jul 10, 2026
7ab8eaf
perf(oev): bound bundle search frontier
1kresh Jul 10, 2026
6c4cf38
docs(rfq): reconcile execution and discount behavior
1kresh Jul 10, 2026
f59d6ca
test(oev): characterize hardened boundaries
1kresh Jul 10, 2026
0ef21aa
fix(3f): normalize max rate exactly
1kresh Jul 10, 2026
7f843e8
fix(3f): sign salted EIP-712 offer domains
1kresh Jul 10, 2026
bd38bf3
test(3f): characterize offer and redemption paths
1kresh Jul 10, 2026
0577168
test(oev): cover pinned IRM read failures
1kresh Jul 10, 2026
746fe4a
docs: reconcile solver hardening behavior
1kresh Jul 10, 2026
6d5b02c
fix(txmanager): exclude unbroadcast replacement hashes
1kresh Jul 10, 2026
6dcd0a7
docs(rfq): correct internal API terminology
1kresh Jul 10, 2026
c413d67
fix(chain): isolate transaction broadcasts from fallbacks
1kresh Jul 10, 2026
0a63524
fix(oev): remove obsolete market sorter
1kresh Jul 10, 2026
da46569
test(txmanager): stabilize replacement deadline cases
1kresh Jul 10, 2026
f8dc18e
docs: clarify public audit port boundaries
1kresh Jul 10, 2026
459bc14
test(txmanager): stabilize blocking receipt boundary
1kresh Jul 10, 2026
6e6a5e8
fix(runtime): propagate nested fatal errors
1kresh Jul 10, 2026
b8efc04
fix(3f): harden redemption scans and offer expiry
1kresh Jul 10, 2026
ae3298b
test(3f): remove constant auction fixture parameter
1kresh Jul 10, 2026
064d927
docs: mark private runtime script steps historical
1kresh Jul 10, 2026
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
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,25 @@ jobs:
- name: Test
run: make test

generated:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # pin@v7.0.0

- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # pin@v6.5.0
with:
go-version-file: go.mod
cache: true

- name: Verify Java
run: java -version

- name: Install code-generation tools
run: make tools

- name: Verify generated code is current
run: make check-generated

lint:
runs-on: ubuntu-latest
steps:
Expand Down
33 changes: 22 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ generic layer, stop — the abstraction is wrong. Generalize the mechanism inste

## Go style (modern Go 1.26)

- Toolchain is pinned: module declares `go 1.26`, builds run `GOTOOLCHAIN=go1.26.4`. Match it.
- Toolchain is pinned: module declares `go 1.26`, builds run `GOTOOLCHAIN=go1.26.5`. Match it.
- **Errors:** use `github.com/go-errors/errors` — `errors.Errorf("...: %w", err)` (NOT `fmt.Errorf`;
`forbidigo` enforces this) and `errors.New` for sentinels. Wrap with `%w` and add context at each
boundary; compare with `errors.Is`/`errors.As`. Return errors, don't log-and-continue silently —
Expand All @@ -76,10 +76,15 @@ generic layer, stop — the abstraction is wrong. Generalize the mechanism inste
operational events; `V(1)` for debug detail. Structured key/values, not formatted strings.
- **Context:** thread `context.Context` through all I/O (RPC, HTTP, tx). Respect cancellation; never
`context.Background()` deep in a call path.
- **Concurrency:** shared on-chain sending goes through the single `txmanager` (nonce-serialized) —
solvers build calldata and submit a request, they never send transactions directly and never race
on nonces. Document the goroutine/locking model of any new shared state (see the `apiClient`
"single Run goroutine" note).
- **Concurrency:** shared on-chain sending goes through the single `txmanager`. Its dispatcher alone
allocates and commits nonces, then constructs, signs, and initially broadcasts each original
attempt; manager-owned trackers may poll receipts and construct, sign, and broadcast same-nonce
replacements concurrently. Solvers build calldata and submit requests, never send directly, and
branch on `Result.State` / `SafeToRetry()` rather than `Err` alone. Every new goroutine must be owned
and joined by its component's `Run` or `Start`. A component whose fatal child must join work owned
by a root sibling reports that error through the generic fatal reporter before joining, so root
cancellation can release the sibling without weakening its outcome contract. Document the
goroutine/locking model of any new shared state (see the `apiClient` "single Run goroutine" note).
- Keep functions at one altitude, prefer small pure helpers (they're the easily-tested seams),
table-driven tests, and accept interfaces / return concrete types. Run `golangci-lint` (below) and
fix findings rather than suppressing them; a `//nolint` must be specific and carry an explanation
Expand All @@ -90,10 +95,10 @@ generic layer, stop — the abstraction is wrong. Generalize the mechanism inste
Nothing merges red. Before considering a change done, all of these must pass:

```
GOTOOLCHAIN=go1.26.4 golangci-lint run --fix # make format — formats + lints + autofixes
GOTOOLCHAIN=go1.26.4 go build ./...
GOTOOLCHAIN=go1.26.4 go test -race -cover ./... # make test
GOTOOLCHAIN=go1.26.4 golangci-lint run # make lint — must report 0 issues
GOTOOLCHAIN=go1.26.5 golangci-lint run --fix # make format — formats + lints + autofixes
GOTOOLCHAIN=go1.26.5 go build ./...
GOTOOLCHAIN=go1.26.5 go test -race -cover ./... # make test
GOTOOLCHAIN=go1.26.5 golangci-lint run # make lint — must report 0 issues
```

- **Unit-test all new logic.** Pure logic (pricing/sizing, EIP-712 digests, config parsing/validation)
Expand Down Expand Up @@ -133,8 +138,10 @@ Three instances of the same pattern — **vendor → generate → commit, regene
(via `hack/openapi-generator-cli.sh`, which downloads the pinned jar on demand — needs a JRE) into
`api/<client>/`. `OPENAPI_GENERATOR_VERSION` is pinned and is the **floor**: it must ingest the spec
(e.g. 7.12.0 for an OpenAPI 3.1 spec with numeric `exclusiveMinimum` / `type:[…,null]` unions, which
`oapi-codegen`/kin-openapi and `ogen` reject). The recipe strips the generator's non-package cruft
(its `go.mod`/docs/test/etc.), keeping only the Go client so it joins the main module.
`oapi-codegen`/kin-openapi and `ogen` reject). The 7.12.0 JAR is verified before execution with
SHA-256 `33e7dfa7a1f04d58405ee12ae19e2c6fc2a91497cf2e56fa68f1875a95cbf220`. The recipe strips the
generator's non-package cruft (its `go.mod`/docs/test/etc.), keeping only the Go client so it joins
the main module.
- **GraphQL clients (schema SDL + operations → genqlient).** Vendor the upstream schema SDL under
`api/graphql/<name>/` (`make refresh-morpho-graphql-schema` pulls Morpho's live schema), keep named
operation documents under `operations/`, then `make refresh-morpho-graphql-client` runs pinned
Expand All @@ -149,6 +156,10 @@ wrappers) stay contained at the boundary and don't leak into solver logic. Reach
**whenever a new integration needs to call a contract or a typed HTTP API** — add the `make` target and
commit the generated output; don't hand-roll request/response structs or `abi.Pack` calls.

Run `make check-generated` after changing a vendored interface or generation recipe. It regenerates all
committed outputs from vendored inputs and rejects both tracked and untracked drift. CI runs this target
only; it never runs the live `refresh-*` targets, so verification never refreshes upstream artifacts.

## Security

Write defensively; this bot holds a signing key and moves funds.
Expand Down
58 changes: 56 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ GENQLIENT_X_TOOLS_VERSION ?= v0.38.0
# Java openapi-generator (downloaded on demand by hack/openapi-generator-cli.sh). 7.12.0 is the floor:
# it ingests OpenAPI 3.1 (the RFQ backend spec); 5.4.0/7.0.1 fail on it.
OPENAPI_GENERATOR_VERSION ?= 7.12.0
OPENAPI_GENERATOR_SHA256 ?= 33e7dfa7a1f04d58405ee12ae19e2c6fc2a91497cf2e56fa68f1875a95cbf220

# Foundry build output to vendor ABIs from (sibling rfq repo by default).
FORGE_OUT ?= ../rfq/out
Expand Down Expand Up @@ -151,7 +152,7 @@ bindings: ## Generate Go bindings from vendored ABIs (grouped per integration; p
# propertyNames — and has dangling oneOf $refs; the generator handles them fine but its strict validator
# rejects them). 3f/rfq keep validation on.
define gen_openapi_client
GO_POST_PROCESS_FILE='gofmt -w' OPENAPI_GENERATOR_VERSION=$(OPENAPI_GENERATOR_VERSION) bash ./hack/openapi-generator-cli.sh \
GO_POST_PROCESS_FILE='gofmt -w' OPENAPI_GENERATOR_VERSION=$(OPENAPI_GENERATOR_VERSION) OPENAPI_GENERATOR_SHA256=$(OPENAPI_GENERATOR_SHA256) bash ./hack/openapi-generator-cli.sh \
generate --enable-post-process-file $(4) -i ./$(1) -g go -o ./$(2) --package-name $(3)
cd $(2) && rm -rf go.mod go.sum .gitignore .openapi-generator-ignore .travis.yml git_push.sh README.md api docs test .openapi-generator
endef
Expand All @@ -178,7 +179,7 @@ refresh-lifi-client: ## Generate the LI.FI order-server client (openapi-generato
tmp="$$(mktemp -p . --suffix=.lifi-normalized.json)"; \
trap 'rm -f "$$tmp"' EXIT; \
python3 hack/lifi-openapi-normalize.py < openapi/lifi-order.openapi.json > "$$tmp"; \
GO_POST_PROCESS_FILE='gofmt -w' OPENAPI_GENERATOR_VERSION=$(OPENAPI_GENERATOR_VERSION) bash ./hack/openapi-generator-cli.sh \
GO_POST_PROCESS_FILE='gofmt -w' OPENAPI_GENERATOR_VERSION=$(OPENAPI_GENERATOR_VERSION) OPENAPI_GENERATOR_SHA256=$(OPENAPI_GENERATOR_SHA256) bash ./hack/openapi-generator-cli.sh \
generate --enable-post-process-file --skip-validate-spec -i "$$tmp" -g go -o ./api/lifiorder --package-name lifiorder
cd api/lifiorder && rm -rf go.mod go.sum .gitignore .openapi-generator-ignore .travis.yml git_push.sh README.md api docs test .openapi-generator

Expand All @@ -202,6 +203,59 @@ graphql-client: refresh-morpho-graphql-client ## Generate GraphQL clients
.PHONY: generate
generate: bindings openapi-client graphql-client ## Regenerate all committed codegen

GENERATED_PATHS := api/bindings api/threef api/rfqbackend api/lifiorder api/morphographql api/graphql/morpho/operations.json

.PHONY: check-generated
check-generated: ## Regenerate committed code and fail on drift without changing the generated tree
@set -euo pipefail; \
generated_paths=( $(GENERATED_PATHS) ); \
preflight_status=0; \
git diff --exit-code HEAD -- "$${generated_paths[@]}" || preflight_status=$$?; \
if (( preflight_status != 0 )); then \
printf '%s\n' "tracked generated files differ before regeneration" >&2; \
fi; \
untracked="$$(git ls-files --others --exclude-standard -- "$${generated_paths[@]}")"; \
if [[ -n "$$untracked" ]]; then \
printf '%s\n%s\n' "untracked generated files before regeneration:" "$$untracked" >&2; \
preflight_status=1; \
fi; \
if (( preflight_status != 0 )); then \
exit "$$preflight_status"; \
fi; \
tmp="$$(mktemp -d "$${TMPDIR:-/tmp}/vault-solver-generated.XXXXXX")"; \
snapshot="$$tmp/generated.tar"; \
tar -cf "$$snapshot" "$${generated_paths[@]}" || { status=$$?; rm -rf "$$tmp"; exit "$$status"; }; \
restore_generated() { \
local status=$$?; \
local restore_status=0; \
local cleanup_status=0; \
trap - EXIT HUP INT TERM; \
set +e; \
rm -rf "$${generated_paths[@]}" || restore_status=$$?; \
if (( restore_status == 0 )); then \
tar -xf "$$snapshot" || restore_status=$$?; \
fi; \
rm -rf "$$tmp" || cleanup_status=$$?; \
if (( restore_status != 0 || cleanup_status != 0 )); then \
printf '%s\n' "failed to restore the pre-generation generated tree" >&2; \
exit 1; \
fi; \
exit "$$status"; \
}; \
trap restore_generated EXIT; \
trap 'exit 129' HUP; \
trap 'exit 130' INT; \
trap 'exit 143' TERM; \
$(MAKE) generate; \
post_status=0; \
git diff --exit-code -- "$${generated_paths[@]}" || post_status=$$?; \
untracked="$$(git ls-files --others --exclude-standard -- "$${generated_paths[@]}")"; \
if [[ -n "$$untracked" ]]; then \
printf '%s\n%s\n' "untracked generated files after regeneration:" "$$untracked" >&2; \
if (( post_status == 0 )); then post_status=1; fi; \
fi; \
exit "$$post_status"

.PHONY: build
build: ## Build the binary
@mkdir -p bin
Expand Down
57 changes: 47 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ are listed under [Solvers](#solvers).
- **`internal/solvers/<name>/`** — one self-contained package per integration; all protocol-specific
logic lives here.
- **`internal/{config,chain,signer,txmanager}`** — solver-agnostic infra: two-stage config, vault /
Multicall3 reads, a pluggable signer, and a nonce-serialized transaction sender shared across solvers.
Multicall3 reads, a pluggable signer, and a shared nonce dispatcher with concurrent transaction
confirmation/replacement trackers.
- **`api/`** — committed codegen: contract `bindings/` (abigen) and protocol API clients, each
refreshable from upstream.

Expand All @@ -29,7 +30,7 @@ the relevant protocol API on each tick; no database.
## Solvers

Solvers are listed in config under `solvers:` — one or more, **at most one entry per solver type**.
Every solver in the process shares the chain client, signer, and the single nonce-serialized
Every solver in the process shares the chain client, signer, and the single nonce-owning
`txManager`, so multiple solvers on one EOA never race on nonces. Each entry's `config` block is typed
and validated by its own solver. Adding a solver touches **no** framework code — see the recipe in
[`CLAUDE.md`](./CLAUDE.md).
Expand All @@ -47,14 +48,17 @@ The `3f-bridge-facilitator`, `rfq-filler`, and `redstone-oev` solvers expose a p
### 3F Bridge Facilitator — `3f-bridge-facilitator`

Acts as a Bridge Facilitator in **[3F (Grunt)](https://3f.xyz)**'s bridge-loan auctions, on top of one
or more Symbiotic `BridgeFacilitatorAdapter`s. 3F auctions the right to front a bridge loan; this solver bids on behalf
or more Symbiotic `ThreeFAdapter`s. 3F auctions the right to front a bridge loan; this solver bids on behalf
of its adapters, funds the loans it wins just-in-time, and permissionlessly redeems repaid loans back
to the vault with yield.

It holds no API key: each adapter is registered with 3F by its vault creator, who sets this solver's
signer as the adapter's EIP-1271 signer, so offers are authorized by signature alone. Design, config,
and roadmap: [`docs/3F-PLAN.md`](docs/3F-PLAN.md) · example
[`config/3f.example.yaml`](config/3f.example.yaml).
[`config/3f.example.yaml`](config/3f.example.yaml). Signed offers default to a lifetime of twice the
discovery interval and cannot be configured to expire before the next discovery pass. Signed
expirations are rounded upward to the next Unix second when needed, so fractional clocks or configured
durations never shorten the requested offer lifetime.

### RFQ Filler — `rfq-filler`

Expand All @@ -63,8 +67,9 @@ An externally-owned solver/executor for **[Symbiotic RFQ](https://symbiotic.fi)*
that fills the orders it is awarded, settling on-chain through the adapter.

It runs either in `external` mode (the open-source filler; quoting and filling scoped to the operator's
own adapters) or `internal` mode (Symbiotic-internal; adds the private discounts flow). The caller EOA
must be an authorized caller of the RFQ `Executor` (its `setCallers` allowlist, granted by the owner).
own adapters, with no discounts API access) or `internal` mode (Symbiotic-internal; may use the
backend's internal-only discounts API). The caller EOA must be an authorized caller of the RFQ
`Executor` (its `setCallers` allowlist, granted by the owner).
Design, config, and roadmap:
[`docs/RFQ-PLAN.md`](docs/RFQ-PLAN.md) · example
[`config/rfq.example.yaml`](config/rfq.example.yaml).
Expand All @@ -75,6 +80,8 @@ An off-chain bidder for **[RedStone Atom OEV](https://docs.redstone.finance/docs
price update makes a **[Morpho Blue](https://morpho.org)** position liquidatable, RedStone runs a
sub-second WebSocket auction for the right to be the liquidator; this solver bids, and on winning, its
signed payload is bundled atomically with the price update and the liquidation.
Its authenticated auction stream requires a `wss://` endpoint in production; plaintext `ws://` is
accepted only for local loopback testing.

On settlement it liquidates the position and exits the seized collateral through a single Symbiotic
`LiquidLaneAdapter`, realizing the spread and paying its bid. It signs and bids but never submits the
Expand All @@ -95,14 +102,19 @@ The solvers split protocol plumbing (reads, signing, submission — fixed) from
- **`webhook`** — delegates each decision to an **external HTTP service you run**: the solver sends it
the raw facts as JSON and executes the plan it returns, so your service owns the logic.

In 3F webhook inputs, `maxRateBps` is an exact decimal string (for example, `"50.5"`), not a JSON
number. Webhook consumers must decode that field as a string.

This is the seam for customizing a solver without forking. Contract and trust model:
[`docs/strategy-plan.md`](docs/strategy-plan.md).

## Requirements

- Go (toolchain version pinned in [`go.mod`](./go.mod); auto-fetched by recent Go releases).
- Go 1.26.5 (toolchain pinned in [`go.mod`](./go.mod); auto-fetched by recent Go releases).
- For regenerating codegen: `make tools` (installs pinned `abigen`, `golangci-lint`). OpenAPI clients use
the Java openapi-generator, downloaded on demand by `hack/openapi-generator-cli.sh` (needs a JRE).
Its 7.12.0 JAR is verified with SHA-256
`33e7dfa7a1f04d58405ee12ae19e2c6fc2a91497cf2e56fa68f1875a95cbf220` before execution.
- A reachable EVM RPC endpoint and a signing key (see Configuration).

## Quickstart
Expand Down Expand Up @@ -130,9 +142,28 @@ implementation and hands the opaque `solver.config` block to that solver to type
own fully annotated example under `config/` (see the *Example config* column above) — every field,
including the shared `chain`/`signer`/`txManager`/`observability` blocks, is documented inline there.
The `chain` block takes a primary `rpcUrl` plus optional `rpcFallbackUrls` — HTTP(S) endpoints tried
in order when the primary is unavailable. **Never commit a real key or live config** — keys are
in order for reads when the primary is unavailable. Transaction broadcasts use exactly one endpoint:
`writeRpcUrl` when configured, otherwise the primary `rpcUrl`; they never traverse read fallbacks.
Startup preflights every distinct read and write endpoint against the configured chain ID; endpoint
errors expose only a safe origin label, never credentials, paths, queries, or fragments. **Never
commit a real key or live config** — keys are
supplied via env/file behind the `Signer` interface; `*.local.*` and `.env` are gitignored.

Generated 3F and RFQ upstream clients reject HTTP response bodies larger than 8 MiB. An oversized
upstream response fails that request instead of being decoded or retained in memory.

The `txManager` dispatcher serializes nonce allocation plus construction, signing, and initial
broadcast of each original attempt. Independent trackers construct, sign, and broadcast any
same-nonce replacements, then require a canonical receipt plus the configured confirmation depth. The shared
`pendingIntervalMs` (default 120000), `feeBumpBps` (default 1250), and `maxReplacements` (default 3)
settings bound same-nonce, same-payload replacements. A positive `maxFeeGwei` is a hard ceiling and is
never exceeded by an initial transaction or replacement. See either annotated example for the exact
bounds.

All long-lived listeners and workers are supervised. An observability or RFQ listener failure is
process-fatal; cancellation shuts down the listeners and joins the transaction manager and solver
workers before the process returns.

## Code generation

Generated code is committed for hermetic builds; refresh from upstream on demand:
Expand All @@ -141,12 +172,18 @@ Generated code is committed for hermetic builds; refresh from upstream on demand
make refresh-abi FORGE_OUT=../rfq/out # re-vendor contract ABIs from a Foundry build
make refresh-openapi # re-pull the live 3F OpenAPI spec
make refresh-rfq-openapi # re-pull the RFQ backend OpenAPI spec
make generate # regenerate bindings + API client
make refresh-lifi-openapi # re-pull/extract the LI.FI order-server OpenAPI spec
make refresh-lifi-client # regenerate only the LI.FI client from its vendored spec
make generate # regenerate all bindings and API clients, including LI.FI
make check-generated # regenerate from vendored inputs and reject drift
```

CI runs `make check-generated` only against committed interface artifacts. It never runs the live
`refresh-*` targets, so upstream changes enter the repository only through an explicit refresh.

## Contributing

Engineering conventions — the modular framework/integration boundary, config-driven configuration,
modern Go 1.26 style, the required test/lint/format gate, and secure-coding rules — are in
modern Go 1.26.5 style, the required test/lint/format gate, and secure-coding rules — are in
[`CLAUDE.md`](./CLAUDE.md) (`AGENTS.md` is a symlink to it). Every change must keep
`make format && make test && make lint` green and unit-test new logic.
Loading