Skip to content

harness: syndication underwriting, emergency stop and live custody totals - #112

Open
valthon wants to merge 21 commits into
masterfrom
feature/syndication-underwriting
Open

valthon wants to merge 21 commits into
masterfrom
feature/syndication-underwriting

Conversation

@valthon

@valthon valthon commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Bootstrap bonded syndication across the depot and both outposts, exercise underwriting policy and emergency recovery through live flows, and restrict launch flow discovery to supported operations.

Design sources: WIRE-V1-SYNDICATION-UNDERWRITING-SPEC.md and WIRE-V1-UNDERWRITING-REQUIREMENTS.md in the platform workspace. Historical test citations below are retained local ledgers/reports under that workspace and refer to the revisions named in those reports. Current producer-collateral validation is recorded separately below.

What changes

Bootstrap and live coverage

  • f58ae6ae — Deploy and configure bond/synd/andon, panic accounts, outpost limits and custody-backed mock imports; add typed tools and migrate liquidity flows, including versioned Ethereum artifact aliases.
  • 2c267652 — Add underwriting, invalid-challenge and rate-limit flows with exact accounting and queue assertions.
  • 5fac93f0 — Add emergency-stop, custody-shortfall and exactly-once pending-payment flows; scope expected freeze handling in the heartbeat monitor and run package unit tests through the root gate.
  • 69bd4afb — Exclude eight launch-disabled reserve/swap flow scripts from local and CI discovery while retaining the packages in the build.

Producer collateral requirements

Commit 686f09cd.

The producer-registration flow now requires 2.0 depot-native LIQSOL AND 2.0 depot-native LIQETH. Both entries use the WIRE chain code and a minimum of 2,000,000,000 atomic units. Custody-backed imported shadow tokens fund the producer, which bonds them through sysio.opreg::deposit.

The flow rejects admission with only LIQSOL bonded, admits the producer once both minima are met, and retains block production, missed-round demotion, and recovery coverage. It withdraws each bond independently to prove that losing either requirement removes eligibility, then checks exact shadow-token repayments and cleared remit claims.

Included from earlier work

  • 4a29b759 — cluster-tool: load every wire-solana program at genesis + stand up the liqsol surface
  • 7308c6da — cluster-tool: drive the liq-syndication surface through the real liqsol_core paths
  • 4418fd7b — flow-liq-syndication: prove the liq attestations end-to-end; drop the injectors
  • 8139bd91 — docs: record the Solana genesis program set, the liqsol surface and the new flow
  • dea084bb — chore(deps): update @wireio/* to latest
  • f2984999 — cluster-tool + flow-liq-yield: bootstrap sysio.swap/sysio.liq and drive the LIQ reward flow end to end
  • 2dd52365 — chore(deps): update @wireio/* to latest
  • a0ae9e79 — fix(jest): size the test ceiling for hosts that serialize bind()
  • 7b4a93e3 — Bond operator collateral on the depot
  • c5dbe99f — Make withheld Solana operations opt-in
  • 265ba21d — Merge origin/feature/liq-yield-flow (PR cluster-tool + flow-liq-yield: bootstrap sysio.swap/sysio.liq and drive the LIQ reward flow end to end #104) into feature/syndication-underwriting

The earlier work includes depot-native collateral and the merge of #104, which is stacked on #98. All commits above are in the current diff against master.

How it was tested

  • Hosted CI for 686f09cd: run 36952381503 failed in Install deps when package preparation compiled against published @wireio/sdk-core@1.0.92, which lacks required syndication/bond/andon generated types. The later Build and Test steps were skipped. Local validation used the companion feature SDK and generated OPP models; standalone CI still requires the matching dependency publications described below.
  • Current producer-collateral update: workspace TypeScript build, edited-source lint, and all 8 producer collateral unit tests passed. A fresh monitored flow-producer-registration run passed 75 phases / 300 steps in 1460.3 seconds, including both mandatory bonds, production, demotion/recovery, independent withdrawal eligibility checks, and exact repayments. Local evidence: /tmp/producer-dual-liq-20261001-r2/reports/cluster-build.md (2026-10-01).
  • Full-suite limitation for 686f09cd: the repository-wide Jest gate was attempted three times, including reduced concurrency and direct execution. Test workers remained blocked in the execution environment's syscall guard, so those runs were interrupted. The full suite remains unverified. The pre-commit hook was bypassed with explicit maintainer approval; the successful build, lint, focused unit tests, and live-flow validation above remain the evidence for this change.
  • Historical E2 per-commit clean build/lint/test gates returned RC 0; reviewed revisions recorded 250 suites / 2574 tests and 252 suites / 2638 tests. Source: wire-sysio/.superpowers/sdd/syndication-underwriting-part-e/task-E2-report.md.
  • The BAR report records a clean build/lint/test gate at 7f7aa6f8: 257 Jest suites / 2731 tests, plus 24 emergency-stop unit tests, totaling 2755 passing tests. Focused BAR alias coverage passed 2 suites / 28 tests. Source: wire-sysio/.superpowers/sdd/syndication-underwriting-part-e/task-bar-report.md.
  • The earlier regression recorded 11 successful flows and an NFT artifact lookup failure. The BAR regression then recorded 13 launch-compatible flows SUCCESS, including node-owner NFT and the default emissions soak; termination and underwriting succeeded on fresh-directory retries after Solana connectivity failures. Sources: wire-sysio/.superpowers/sdd/syndication-underwriting-part-e/regression-report.md, wire-sysio/.superpowers/sdd/syndication-underwriting-part-e/progress.md and wire-sysio/.superpowers/sdd/syndication-underwriting-part-e/task-bar-report.md.
  • Eight reserve/swap-dependent flows hit Solana OperationDisabled (6086) because wire-solana 89565920 withholds those operations at launch. Their disabling was recorded as 1033f312 and is carried by current commit 69bd4afb. These are disabled launch scenarios, not successful runs. Sources: wire-sysio/.superpowers/sdd/syndication-underwriting-part-e/task-bar-report.md and wire-sysio/.superpowers/sdd/syndication-underwriting-part-e/task-manifest-report.md.
  • Earlier report-backed flow and unit results belong to their recorded revisions. The current producer-collateral validation above applies to this update; it does not rerun all historical live flows.

Landing order and dependencies

  1. Merge wire-sysio first, then publish wire-opp-solana-models, @wireio/opp-solidity-models, and @wireio/opp-typescript-models with the new total_syndicated field.
  2. Land wire-solana and wire-ethereum after those packages publish. Their standalone CI cannot compile against the older models; Ethereum also requires @wireio/opp-solidity-models ^1.0.51. Solana targets codex/operation-disabled; Ethereum targets next.
  3. Land wire-libraries-ts after wire-sysio so its generated contract types match the deployed ABIs.
  4. Land wire-tools-ts after wire-libraries-ts and after cluster-tool + flow-liq-syndication: drive the simple_swap liq attestations through the real liqsol_core paths #98 and cluster-tool + flow-liq-yield: bootstrap sysio.swap/sysio.liq and drive the LIQ reward flow end to end #104 (cluster-tool + flow-liq-yield: bootstrap sysio.swap/sysio.liq and drive the LIQ reward flow end to end #104 is stacked on cluster-tool + flow-liq-syndication: drive the simple_swap liq attestations through the real liqsol_core paths #98).
  5. Land wire-platform-manifest last, aligning documentation and rules with the runtime changes.

Ethereum includes the SyndicationPool commit from draft https://github.com/Wire-Network/wire-ethereum/pull/207 unchanged as 8ed70fb8; subsequent changes extend and harden it.

Related PRs

🤖 Generated with Claude Code

https://claude.ai/code/session_01WvJpbenHM8A16MLiMteYtj

valthon and others added 17 commits August 20, 2026 14:54
Concurrent clusters on one host race the cross-process bind registry.
The advisory port lock now keys on the NORMALIZED registry path — two
spellings of one directory hash to one lock — and lives in the OS temp
dir rather than inside the registry: a sandboxing caller owns and
deletes that directory, while proper-lockfile keeps an mtime-refresh
timer alive for a held lock and its onCompromised hook THROWS when the
directory vanishes underneath it (`ENOENT ... wire-cluster-ports.lock.lock`).
Hashing the path keeps the scoping while putting the file somewhere no
test owns.

Real runs are unchanged: with the env var unset every process resolves
the same default registry, hence the same lock, and the host-global
cross-process guarantee holds exactly as before. A caller that sandboxes
the registry (every jest suite, via tests/jest.setup.ts) gets its OWN
lock, which is correct — disjoint registries cannot collide on a port,
so serializing them protects nothing.

The stale-lock steal window is raised 10s -> 30s so it sits ABOVE the
~15s loaded-host port-probing critical section; at 10s a live holder was
stolen mid-section, which is the failure mode that let two clusters draw
the same port and UDP-double-bind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…timer throw

Mechanism, verified against proper-lockfile@4.1.2 `lib/lockfile.js`:
`updateLock` refreshes a held lock's mtime on an `unref`'d timer; when that
`stat`/`utimes` ENOENTs it calls `setLockAsCompromised` with
`Object.assign(err, { code: 'ECOMPROMISED' })` — the error is rewritten IN
PLACE, so its `ENOENT … stat '…/<target>.lock'` text survives. The DEFAULT
`onCompromised` is `(err) => { throw err }`, i.e. an uncaught exception raised
from that timer callback, with no call-stack relationship to the code holding
the lock: under jest it fails whatever test happens to be running, while the
caller still inside the critical section is never told it lost exclusivity.
`setLockAsCompromised` also marks the holder released before invoking the
handler, which is why `release()` then rejects `ERELEASED` (already handled).

The fix: `withFileLock` owns `onCompromised`. It logs, then rejects a
`Deferred` the call races against the section, so the compromise fails THAT
call with a `NestedError` carrying proper-lockfile's error as the cause and
`{ lockPath }` as context. A compromise landing after the section already
settled is swallowed; a section that fails after a compromise decided the
outcome is logged at warn instead of vanishing. The section itself cannot be
cancelled — the public JSDoc now says so, since its side effects still land.

Deliberately NOT changed: `stale`, `update` and `retries` stay exactly as
master had them. Across four full cluster-tool runs the compromise signature
appeared 18–68 times while `Unable to update lock within the stale threshold`
— the signature a stale STEAL produces — appeared ZERO times: every observed
compromise was the lock directory being deleted under a live holder, so the
handler, not the threshold, is what fixes the reported failure. Raising
`stale` would only lengthen how long a crashed holder wedges every contender.

Gate on an idle host: `pnpm build` clean, `pnpm run lint` clean,
`pnpm --filter @wireio/cluster-tool test` 138/138 suites, 1505/1505 tests,
with no `.lock.lock` signature anywhere in the run. The new compromise test
fails against the unpatched handler (uncaught `ENOENT … .lock` from the
refresh timer; the call never settles), and the root `jest.config.ts` note
about that hook throwing is updated to what happens now.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CNDguHHWTwn1UBejDiTAGa
…e liqsol surface

The Solana outpost bootstrap only ever loaded `liqsol_core`, so the liqsol
staking + syndication surface it hosts had no accounts behind it: the liqSOL
mint, its Token-2022 transfer hook, the distribution / stake / withdraw state,
the leaderboard, the wire `GlobalState` and the reserve pool simply did not
exist on a flow cluster. Anything that drives a REAL `synd` or
`report_liq_yield` was therefore unreachable.

Load all four programs at genesis and run wire-solana's own `init-*` scripts:

- `SolanaOutpostProgramTool` generalizes over the program's CRATE name
  (`.keys/<name>-keypair.json`, `target/deploy/<name>.so`,
  `target/idl/<name>.json`) and gains `assertIdlProgramId`, so the id a script
  resolves from the IDL can be compared to the id the validator loaded.
  `liqsol_core` stays the default, so every existing caller is unchanged.
- `SolanaValidatorProcessSteps.resolvePrograms` returns all four programs
  (`GenesisAnchorPrograms`), each upgradeable under the ONE per-cluster
  deployer — the same identity `initialize_global_config` proves against
  `ProgramData`.
- `SolanaAnchorScriptTool` runs ONE `Anchor.toml` `[scripts]` entry per Step
  (`anchor run <script> --provider.cluster … --provider.wallet …`), the Solana
  analogue of the hardhat shell-out in `EthereumOutpostBootstrapper`. It
  prepends wire-solana's own `node_modules/.bin` to the subprocess `PATH`:
  every `[scripts]` entry is a bare `ts-node`, which anchor-cli hands to
  `bash -c` WITHOUT adding the workspace bin dir, so on a machine with no global
  `ts-node` — a CI runner — every script would die `command not found`.
  Prepending it also means the interpreter is the one that repo's lockfile pins
  rather than whatever a global shim happens to be.
- `SolanaLiqsolSurfaceSteps.planLiqsolSurface` composes the phase: assert the
  toolchain and the program ids, airdrop the deployer, run the sixteen init
  scripts in `bash-scripts/reset-local-cluster.sh`'s order — with harness Steps
  spliced in at the three scripts that need them — then top the rent treasury up
  to its floor.

  `init-distro` and `init-controller` are NOT among the scripts. Both write
  unconditionally and both end in `main().catch(console.error)`, so they exit 0
  on a failed write, and `runScript` sees only the exit code: their Report rows
  could not go red, and a failed init would surface at whichever later step
  first read `distribution_state` or `controllerState`, under the wrong name.
  The three writes they carry are three harness Steps instead (`initialize`,
  `initialize_stake_controller_state`, `initialize_vault`), each reading its
  target account first, at the position the scripts ran — which is not one
  position: `init-distro` runs immediately BEFORE `init-wire-config`, whose
  instruction reads the `distribution_state` it creates, and `init-controller`
  immediately before `init-global-config`. The Steps are keyed to those script
  NAMES rather than to an index, so inserting an init script cannot move them.

  `init-pretoken-purchase-history` gets a gate and a read-back, and this one is
  not hygiene either. The instruction stores `starting_epoch = current_epoch -
  1`, and `PretokenPurchaseHistory::is_initialized()` IS `starting_epoch != 0`.
  In Solana epoch 0 that underflows and the script exits 1 — every local-mode
  bootstrap dies. In epoch 1 it SUCCEEDS and stores 0, the program's own
  "never initialized" sentinel, after which every pre-launch syndication,
  pretoken and refund path on that cluster fails `InvalidWireState` for the rest
  of its life. Measured on this branch's own runs the step landed 1.4 s into
  epoch 1 — the sentinel, every time, invisible only because PostLaunch `synd`
  never reads the history. So the phase waits for epoch
  `MinimumPretokenHistoryEpoch` (2, wire-solana's own `MIN_EPOCH` in
  `bash-scripts/wait-for-validators.sh`, for exactly this reason) immediately
  before the script, and reads the account back immediately after so a
  regression in the gate cannot pass silently.

  The wait's budget comes from `getEpochInfo()` — the slot time actually
  REMAINING (`slotsInEpoch - slotIndex`, plus whole epochs after it) times a
  slack factor — not from a whole number of epochs counted at the step. By the
  time this runs the scripts ahead of it have already spent most of the wait, so
  a step-relative budget would be mostly slack in the normal case and still too
  tight for a slow validator in the bad one. The read-back polls for a few slots
  before decoding, because the script confirms its `.rpc()` at `processed` while
  the harness reads at `confirmed`. This is also what makes the
  100-slot epoch schedule load-bearing rather than cosmetic: at agave's default
  the wait would be days.

  The toolchain assertion is not hygiene. Since anchor-cli 0.30, `Anchor.toml`'s
  `[toolchain]` is applied on EVERY `anchor` invocation, so a `solana` or
  `anchor` that differs from the pins makes each script Step run
  `agave-install init <pinned>` first — a network download that repoints
  `~/.local/share/solana/install/active_release` while THIS cluster's
  `solana-test-validator` is running out of it. Refusing up front costs one
  `--version` call per binary; discovering it the other way is a validator that
  misbehaves mid-bootstrap, blamed on whichever step was unlucky. `reset-local-cluster.sh` itself is NOT used — its
  `anchor deploy` would fight the genesis load with a different upgrade
  authority. The treasury is NOT one of the scripts: `fund-treasury` transfers
  a fixed amount every time it runs, so on a re-run it would keep paying. A
  script Step is only sound when the script is idempotent, so the treasury gets
  a harness Step instead — `planFundTreasury` reads the PDA's balance and
  transfers only the shortfall, the shape `planKeypairAirdrop` already uses.
  `init-tranche-state` is left out for a harder reason: outside
  `--features development`, `initialize_tranche_state` pins its `chainlink_feed`
  and `chainlink_program` accounts to the real MAINNET Chainlink SOL/USD feed
  and program by address, and a test validator hosts neither — so running it
  would make a development build a prerequisite of the bootstrap. Nothing on the
  syndication path reads the tranche state, and neither does any later script
  here, so the surface is complete without it.
- `SolanaAnchorScriptTool` records each script's stdout/stderr TAILS into the
  step's Report `extra`. A script Step's evidence is its output, and the tails
  are logged at `debug` (twenty scripts x 4 000 chars would flood the aggregate
  log the heartbeat greps); without the Report entry a PASSING step would leave
  nothing behind but the argv that launched it, so `init-wire-config` standing up
  the wire `GlobalState` and the liqSOL pool's accounting would be unverifiable
  after the run.
- `SolanaFundingTool` gains handle-addressed per-cluster keypairs
  (`sol-<name>-keypair.json`, the deployer being one of them) and
  `planKeypairAirdrop`, the operator-free counterpart of `planAirdrop`: the
  deployer and flow-owned wallets have no `ctx.keyStore` operator identity.
- `SolanaValidatorProcess` now passes `--slots-per-epoch`, resolved from the
  PERSISTED `ClusterConfig` so `create`, `run` and `start.sh` cannot drift from
  one another. The field is REQUIRED and carries no schema default:
  `ClusterConfigProvider.resolve` always writes it (from
  `--solana-slots-per-epoch`, defaulting to 100), so a config without it is
  malformed rather than old.
  agave's default keeps a fresh validator at Solana epoch 0 for ~2 days of slot
  time, and `initialize_pretoken_purchase_history_handler` seeds its history
  from `current_epoch.checked_sub(1)` — which underflows (`Underflow`, 7418)
  for as long as the epoch is 0, so the liqsol surface could not be
  initialized at all. This is the same reason wire-solana's own
  `run-wire-postlaunch-local.sh` passes the flag. No harness or flow code
  reads the Solana epoch (the OPP consensus boundary is the DEPOT's epoch), so
  shortening it changes no behavior under test.

The phase is sequenced BEFORE the OPP outpost deploy because that is where
`init-global-config` creates the `global_config` every OPP admin op is gated
on; `SolanaOutpostBootstrapper.ensureGlobalConfig` then finds it already there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CNDguHHWTwn1UBejDiTAGa
…ol_core paths

`SolanaLiqSyndicationTool` is the harness's Step palette for the `liqsol_core`
instructions a user, an admin and a permissionless cranker actually call —
`sol_to_liqsol`, `set_wire_state`, `set_token_address`, `synd`,
`inject_bonus_synd_yield` and `report_liq_yield`. Every one is its own Step, so
the Report records each write; `SYNDICATE_LIQ` and `LIQ_YIELD` are queued by the
program itself, never by the harness.

- PDA derivation is a pure value helper (`deriveBasePdas` / `deriveUserPdas`)
  over `LiqsolPdaSeed` — the seed registry the bootstrap phase that CREATES
  these accounts already derives from, aliased here as `PdaSeed` so every call
  site reads the same. The seeds are cross-checked against
  `liqsol-core`, `liqsol-token` (the mint + its authority) and `transfer-hook`
  (the `ExtraAccountMetaList`). The mint's token accounts are Token-2022 ATAs.
- `readLiqYieldState` and `readWireState` decode `GlobalState` through the
  program's OWN Anchor coder, reached via a connection-only
  `SolanaOutpostProgramTool.loadReadOnlyProgram` — a read needs the IDL's
  layouts and an RPC, never a wallet or a fabricated keypair. It has to be a
  `Program` and not a bare `anchor.BorshCoder`: `Program` is what camelCases the
  IDL, so its coder keys `globalState` and `liqYieldReported` where a raw-IDL
  coder keys `GlobalState` and `liq_yield_reported`, and Anchor exports no
  converter to bridge them. A committed `GlobalState` fixture pins that, because
  the wrong spelling costs a four-minute cluster to discover. The crank's
  reported amount IS the delta between its watermark before and after.
- `set_token_address` is exposed as a Step because it is a hard precondition,
  not a convenience: `synd` and `report_liq_yield` resolve their attestation's
  depot token code via `OutpostConfig::token_code_for_mint` and refuse with
  `LiqTokenNotMapped` when the liqSOL mint is unmapped.
- `SolanaAnchorEnumTool` gains the `WireState` identity enum, its variant tag
  and the program's OWN transition table, alongside the existing proto-enum
  variant helpers. `runSetWireState` READS `GlobalState.wireState` and refuses
  before it submits — `set_wire_state` is a one-way ratchet in `liqsol_core`,
  so a re-run or a wrong target would otherwise spend a transaction to earn an
  opaque on-chain revert.
- Every instruction's account map is an exported pure builder, and one test per
  instruction feeds the runner's OWN map to `program.methods.<ix>().accounts()`
  against a trimmed, committed copy of the real `liqsol_core` IDL, asserting
  the resolved keys, their order and their writable/signer flags. wire-solana
  sets `resolution = false`, so Anchor fills in NOTHING: a renamed key would
  otherwise surface only as a run-time failure deep inside a flow, and an extra
  key would be silently dropped. The same comparison runs at BOOTSTRAP, against
  the IDL actually on disk (`verify-instruction-accounts`), because the
  committed fixture can only prove the maps were right when it was generated —
  and it names the instruction and the drifted keys rather than reverting later.

The deposit Step stands off Solana's epoch-rewards window, and retries once if
it lands in it anyway.
The runtime distributes an epoch's staking rewards over the first blocks of the
next one (SIMD-118) and refuses every stake instruction while it does;
`liqsol_core` reads the same `EpochRewards` sysvar and fails `deposit_to_reserve`
with `EpochRewardsActive`. At the harness's 100-slot epochs that window recurs
every ~40 seconds, so `runDepositForLiqsol` polls the sysvar and waits it out —
otherwise the flow is a coin flip, which is exactly how it was observed failing.

Polling narrows the window to the gap between the last read and the transaction
landing; it cannot close it. So the submission runs through
`submitWithEpochRewardsRetry`, which on that one error waits the window out and
re-sends EXACTLY once — safe in both shapes it arrives in, because a preflight
refusal sent nothing and an on-chain failure is atomic. All three renderings are
matched, derived from the single `EpochRewardsActiveErrorCode`: the AnchorError
name, `custom program error: 0x1dc5` from a refused preflight, and
`{"InstructionError":[1,{"Custom":7621}]}` from a transaction that PASSED
preflight and executed inside the window — the last being the one that actually
happens to the ~1 % of deposits the poll cannot protect. The retry takes its
`send` as a parameter so the branch is exercised without a validator.

`oppEnvelopeScan.readEnvelopeAttestations` returns the raw attestation payloads
of one type on one edge, so a scenario can assert on an attestation's CONTENT
instead of only its type tag. It delegates to `@wireio/debugging-shared`'s
`readEnvelopeRecordsFromDir` rather than re-walking the directory and re-parsing
the envelopes: that reader already owns the `.data`/`.metadata` pairing, the
filename grammar and the torn-read handling. Decoding the payload stays at the
call site, with the generated message class, so this module re-declares no proto
shape.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CNDguHHWTwn1UBejDiTAGa
… injectors

`flow-liq-syndication` now drives the outpost's own instructions instead of
enqueueing attestations behind the program's back. A verify step reads
`GlobalState.wireState` and asserts the outpost is still PreLaunch — FIRST,
because every step after it writes — then a user is funded and deposits SOL for
liqSOL, the admin flips the outpost Launching -> PostLaunch, the user
syndicates — and `synd` queues `SYNDICATE_LIQ` itself. A permissionless donation
then credits the syndicated pool and a NON-ADMIN crank of `report_liq_yield`
queues `LIQ_YIELD`. The circulated `LIQ_YIELD` is DECODED and must carry exactly
the delta the crank advanced its watermark by, on exactly the sequence it drew,
under the outpost's chain code and the depot's liqSOL token code.

Because `set_wire_state` is a one-way ratchet, the scenario is single-shot per
cluster — the flow's README says so, and a second run against the same cluster
fails at that verify step with the state it found rather than at an opaque
on-chain revert several steps later. The step leads the phase so a refused run
spends nothing: behind it sits a real 5 SOL `sol_to_liqsol`, which would
otherwise land before the refusal.

The scenario binds the REAL liqSOL mint to the depot's liqSOL token code as its
first step rather than in the shared bootstrap: the bootstrap binds that code to
the mock SPL mint the swap flows hold reserves in, and only this flow's cluster
should see the change (`wire-outpost-repos-…-belong-in-before-all.md`).

`flow-yield-distribution` loses its Solana leg. The SOL staking surface is a
separate developer track and `liqsol_core` emits no `STAKING_REWARD`, so the leg
could only ever have been an injection; the flow is Ethereum-only until the
outpost produces the attestation itself.

With both consumers gone, the injection surface goes with them:
`SolanaYieldEmitterTool` (and, from the superseded revision of this branch,
`SolanaAddAttestationTool` / `SolanaSyndicationTool` and the flow's own emit
steps) are deleted. Nothing in the harness writes to a program's outbound
message buffer any more.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CNDguHHWTwn1UBejDiTAGa
…he new flow

The root README's outpost summary and `--solana-path` row said the harness
bootstraps `opp-outpost`; it now loads all four wire-solana Anchor programs at
genesis and runs their `init-*` scripts before the OPP deploy. CLAUDE.md's
orchestration map names `SolanaLiqsolSurfaceSteps` and why it is sequenced ahead
of the outpost bootstrap, and the flow table rows say what the two touched flows
actually assert.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CNDguHHWTwn1UBejDiTAGa
sdk-core and shared to 1.0.92, the wire-libraries-ts release carrying
the regenerated SysioContractTypes for sysio.liq and sysio.swap
(wire-libraries-ts#84). Produced by scripts/update-wireio-deps.mjs.

Change-Id: I2ccd90c14dd35080a9961f615041c70ff86a0e52
…ve the LIQ reward flow end to end

The bootstrap configures the swap, opens one shadow symbol per registered liq
token and sets the kicker; the mock yield pools are opt-in
(--enable-mock-liq-pools), seeded inside the epoch-0 window like the mock
reserves. Steps.contracts.sysio.{liq,swap} are the per-action factories;
Steps.user.planProvisionWire is the shared WIRE-user provisioning
flow-swap-from-wire carried locally.

flow-liq-yield: synd -> parked -> createlink sweeps it -> report_liq_yield ->
liqpending -> the batch operators' queueyield and tickyield -> claim pays
exactly owed(row, index) -> desyndicate -> DESYNDICATE_LIQ pays the liqSOL ATA.

Two harness fixes the run surfaced: confirmSignature checked a tx's err only
after returning on a confirmed status, so an on-chain failure passed as
success; WireClient.symbolCodeKeyRange reads a KV row keyed by one symbol_code
exactly (a bare code as a bound is a nodeop parse error).

.pnpmfile.cjs takes WIRE_LIBRARIES_TS_PATH / WIRE_SYSIO_PATH so a worktree links
its sibling worktrees. Pairs with wire-sysio#634 and wire-libraries-ts#84.

Change-Id: I67ceaa5927bb8deb4a71ed70426e223e41a44b22
sdk-core and shared to 1.0.92, the wire-libraries-ts release carrying
the regenerated SysioContractTypes for sysio.liq and sysio.swap
(wire-libraries-ts#84). Produced by scripts/update-wireio-deps.mjs.

Change-Id: I803eca9e735ebf7082843418b8945adfb537fac0
The jest ceiling is a local-operation ceiling, sized to the loaded-host
worst case. For a port-resolving test that worst case is set by bind(): a
resolve is about 800 binds (about 1500 once its process has already claimed
the first dynamic-range window), and on a host that serializes bind() the
cost of each one grows with the number of processes binding at once.

Measured on WSL2 with mirrored networking: one bind costs 10.2 ms with 1
process binding, 135.5 ms with 13 and 324.1 ms with 31. A full run makes
53-56k binds, and its 13 port-resolving suites start together, so every
resolve takes 120-135 s. Against the 120 s ceiling that fails 9-10 suites
with hook and test timeouts, and each timeout then produces the
`ENOENT ... wire-cluster-ports.lock.lock` follow-on the comment already
describes.

Raise `testTimeout` to 360 s in the root config, which multi-project mode
honors, and in the cluster-tool config kept in sync with it. The worker
count bounds how many resolves run at once, which puts the worst case near
31 resolves at about 10 s each. Wall time is unchanged: the run is bound by
bind throughput and a passing test returns the moment it finishes (704 s
failing at 120 s, 708 s passing at 360 s).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvJpbenHM8A16MLiMteYtj
Operator collateral at launch is bonded on the depot. Add typed
sysio.opreg deposit, withdraw and claimremit steps and compose them in
WireCollateralTool with WIRE funding and exact balance verification.
Resolve operator accounts at run time, require a new withdrawal queue
entry, reject truncated reads and budget claim polling by epoch length.

Move collateral lifecycle, producer registration and batch termination
flows to WIRE bonds, including eligibility transitions and exact claim
payouts. Remove the unused Ethereum withdrawal helpers and write budget.
Underwriter plans now fund and bond native WIRE; shadow tokens still
lack a funding path and emit a warning. Keep action and tool tests with
these behaviors and update the consuming flows' descriptions.
The launch Solana program rejects reserve initialization and mock SPL
reserve provisioning with OperationDisabled. Default bootstrap must
skip those calls while retaining the inbound outpost path.

Carry enableLaunchWithheldOperations from the CLI and scenario defaults
through persisted configuration into the bootstrapper. Keep it false
for both new and legacy configurations. Reserve and swap scenarios opt
in explicitly and require a program build without launch restrictions;
depot collateral flows need no opt-in.

Keep registry seeding compatible with an absent sol-mock-mints.json.
Include CLI, schema, bootstrap and registry coverage, update fixtures,
and document the option and its limits for flow authors.
…n-underwriting

PR #104 is the only branch that deploys sysio.liq and sysio.swap in the
harness; the syndication-underwriting work builds on it.

Textual conflicts, resolved by keeping both sides' opt-in flags
(enableLaunchWithheldOperations from this branch, enableMockLiqPools from
#104): CLAUDE.md, README.md, ClusterConfig.ts and its test,
ClusterBuildOptionsArgs.ts and its test, ClusterBuildOptions.ts,
ClusterConfigProvider.ts, clusterConfigFixture.ts, fixtureCluster.ts and
ClusterSlice.test.ts.

Semantic conflicts:
- RegistrySteps.ts and the RegistrySteps / LiqContractSteps tests: the
  depot-native-collateral ABIs spell chain_code and token_code as slug
  codenames, so the shadow and liq-pool rows carry the codename strings.
- Constants.ts: ProtocolTiming.OutpostWriteBudgetMs, which this branch
  dropped as unused, is restored with its tests; #104's two liq flows
  size their outpost write Steps with it.
- oppEnvelopeScan.test.ts: the regenerated OPP bundle's LIQYield carries
  total_syndicated; the fixture sets it and the decode test reads it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvJpbenHM8A16MLiMteYtj
Liquidity intake now needs a bonded envelope before the depot can
release shadow or yield. Bootstrap sysio.bond, sysio.synd and
sysio.andon together, arming panic accounts and configuring each outpost
before traffic starts.

Add typed depot and outpost tools for bonding, custody, emergency
controls and deferred payments. Keep mock position imports opt-in, back
all seeded shadow in custody, and initialize Ethereum principal to avoid
treating that backing as yield.

Migrate the existing liquidity flows to held-envelope bonding and the
sysio.synd actions. Share their user steps and include bootstrap, tool
and configuration coverage alongside the updated flow documentation.
Add fresh-cluster scenarios for bonded release and fees, invalid
challenges with forfeiture, and rate-limited intake and redemption.
Assert exact balances, supply, custody and queue transitions, including
same-epoch crank behavior and restoration of governance configuration.

Share provisioning and state readers in SyndicationScenario, with helper
coverage, and register all three packages in the TypeScript build. Keep
each scenario's run instructions and typed verification steps with it.
Add a live scenario that freezes the Solana and Ethereum outposts, pulls
the depot cord, and verifies that deferred redemptions pay exactly once
after recovery. Exercise automatic custody-shortfall stops and preserve
the mismatch evidence while proving repaired messages are admitted.

Teach the heartbeat monitor to recognize deliberate freeze refusals only
when explicitly requested, while retaining unrelated failure and
liveness checks. Base the initial growth deadline on epoch chain time.

Include monitor and recovery-helper coverage, run package test:unit
scripts through the root test gate, and document the paired live runner
and monitor commands.
The launch Solana program withholds the reserve and swap instructions
needed by eight flows. Rename their live scripts to test:disabled and
discover only packages with a test script, so exact names, regex matches
and the picker all select runnable flows consistently with CI.

Keep disabled packages in the build and document the manifest change
needed to enable them after audit. Include discovery coverage and update
the examples to use an enabled collateral flow.
Complete held principal and yield releases in syndication, challenge, and emergency-stop flows using explicit governance resolution for unbonded envelopes. Wait for the actual external destination balance after desyndication. Retain provider-path coverage and add exact-balance regressions and the enabled-flow audit.
…syndication-underwriting

* commit 'abf6d1ebeb6a9be223ba14c3caac66d93f49419a':
  Scope the bind-registry port lock to the registry it guards
Combine owning-call compromise failures with PR #76's registry-scoped
external mutex and 30-second short-lock stale threshold. Preserve the
long-lock policy and document that lock loss cannot cancel ongoing work.

Retain faster wildcard port probing with IPv6 checks, early range rejection,
two Jest workers, and a supervised full commit gate. Isolate the validator
environment test and keep all test ports in the bind registry.

Cover real lock refresh and mutual exclusion after registry deletion,
late completion after compromise, network-family collisions, and watchdog
termination of descendants.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants