Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,8 +369,8 @@ the backing and custody to cover it before the epoch relay can realize yield.
**Flow authoring — underwriting, syndication and the emergency stop.** The
bootstrap deploys `sysio.andon`, `sysio.bond` and `sysio.synd` and configures them
unconditionally: `PanicAccount` + `EmergencyStop` (after `BootstrapNodeOwner`: the
panic account `Constants.PANIC_ACCOUNT`, `sysio.andon::setpanic`,
`addpuller(sysio.synd)`) and `SyndicationConfig` (after `LiqConfig`:
panic account `Constants.PANIC_ACCOUNT`, funded native `pull`/`clear` permissions
and action links) and `SyndicationConfig` (after `ShadowLiqTokens`:
`sysio.bond::setconfig` and one `sysio.synd::setconfig` per shadow pair, from
`Steps.registry.SyndicationConfigRegistrations`, with no fees). A flow that needs a
fee or another bucket sets its own `synd::setconfig` in `plan()` — governance, not
Expand Down
23 changes: 6 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ pnpm workspace (no nx/turbo/lerna); everything lives under `packages/`.
| `cluster-tool` | `@wireio/cluster-tool` | Core harness: process managers, chain clients, bootstrap, **`wire-cluster-tool` CLI** |
| `flow-operator-collateral-deposit` | `@wireio/test-flow-operator-collateral-deposit` | Node-operator collateral deposit + withdraw remit |
| `flow-batch-operator-termination` | `@wireio/test-flow-batch-operator-termination` | Batch-operator termination via delivery underperformance |
| `flow-yield-distribution` | `@wireio/test-flow-yield-distribution` | `STAKING_REWARD` (Ethereum) → `sysio.dclaim::onreward` → `fundclaim` |
| `flow-liq-syndication` | `@wireio/test-flow-liq-syndication` | Real syndication reaches the destination wallet; reported yield is fully released |
| `flow-liq-yield` | `@wireio/test-flow-liq-yield` | Syndicated liqSOL parked → linked → credited; reported yield minted, sold through `sysio.swap`, claimed as WIRE; `DESYNDICATE_LIQ` paid on the outpost |
| `flow-emissions-soak` | `@wireio/test-flow-emissions-soak` | Multi-hour emissions + `sysio.dclaim` payout soak |
Expand All @@ -157,10 +156,9 @@ and outbound queue entries are intermediate checks.
| `flow-syndication-rate-limit` | Actual bond and FIFO/bucket assertions; accepted redemption must reach the external wallet |
| `flow-emergency-stop` | Actual bond/challenge and deferred payouts; repaired-custody probe deposits must also reach the wallet |

The other seven enabled flows (`flow-batch-operator-slashing`,
The other six enabled flows (`flow-batch-operator-slashing`,
`flow-batch-operator-termination`, `flow-emissions-soak`, `flow-node-owner-nft`,
`flow-operator-collateral-deposit`, `flow-producer-registration`,
`flow-yield-distribution`) submit no user LIQ syndication/desyndication. Imported
`flow-operator-collateral-deposit`, `flow-producer-registration`) exercise their own paths. Imported
bootstrap collateral and reward funding have their own assertions. The eight
former disabled swap/reserve flows have been removed.

Expand Down Expand Up @@ -424,19 +422,10 @@ depot needs them too:
|---|---|
| `OPPContracts` | deploys `sysio.andon` before the contracts that read its cord (`sysio.swap`, `sysio.liq`, `sysio.bond`, `sysio.synd`), and `sysio.bond` before `sysio.synd`, all privileged through `setsyscode`; `OPPCodeGrants` then grants `@sysio.code` to `sysio.bond` and `sysio.synd` (`sysio.andon` sends no inline action) |
| `PanicAccount` | creates `andon.panic`, the account that may pull and clear the cord besides `sysio` |
| `EmergencyStop` | `sysio.andon::setpanic(andon.panic)`, then `addpuller(sysio.synd)`, so a custody shortfall pulls the cord — both before any liq token is registered |
| `SyndicationConfig` | after `LiqConfig`: `sysio.bond::setconfig` (hold bond 1000 bps), one `sysio.synd::setconfig` per shadow pair (`ETHEREUM`/`LIQETH`, `SOLANA`/`LIQSOL`: no fees, buckets of one million tokens, a 60 s challenge window, a one-token challenge charge), then verifies that each shadow is at the depot's 9 decimals and each pair is an active liq token with an active binding |

`setpanic` and `addpuller` run after the four cord readers are deployed, not
before them as `docs/contract-upgrade-order.md` orders a live upgrade: the panic
account must exist first, and the bootstrap creates it only once the bootstrap
node owner can grant its resource policy, which is after `OPPContracts`. The
order is still safe, because what the rule protects is traffic: a reader that
runs before `setpanic` reads the cord as clear, and during the bootstrap no
traffic reaches the readers: the liq tokens are registered only after
`EmergencyStop`, and no envelope is accepted until `EpochBootstrap` starts the
first epoch. By then the panic account is named and `sysio.synd` is a registered
puller.
| `EmergencyStop` | funds and delegates native `pull`/`clear` permissions to `andon.panic` and `sysio`, then links each action before any liq token is registered |
| `SyndicationConfig` | after `ShadowLiqTokens`: `sysio.bond::setconfig` (hold bond 1000 bps), one `sysio.synd::setconfig` per shadow pair (`ETHEREUM`/`LIQETH`, `SOLANA`/`LIQSOL`: no fees, buckets of one million tokens, a 60 s challenge window, a one-token challenge charge and shared 1,000,000-unit gross return minimum), then verifies that each shadow is at the depot's 9 decimals and each pair is an active liq token with an active binding |

Native action permissions are armed after the cord readers deploy and before registry setup or epoch traffic. Privileged `sysio.synd` can pull the cord without a contract-managed puller registry.

Each outpost gets its own emergency stop and per-transfer syndication maximum
in local mode:
Expand Down
2 changes: 1 addition & 1 deletion docs/local-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ Each `flow-*` package is a standalone scenario that bootstraps its own cluster.
| `flow-batch-operator-slashing` | `flow-liq-syndication` | `flow-syndication-challenge` |
| `flow-batch-operator-termination` | `flow-liq-yield` | `flow-syndication-rate-limit` |
| `flow-emergency-stop` | `flow-node-owner-nft` | `flow-syndication-underwriting` |
| `flow-emissions-soak` | `flow-operator-collateral-deposit` | `flow-yield-distribution` |
| `flow-emissions-soak` | `flow-operator-collateral-deposit` | |
| `flow-producer-registration` | | |

Launch one with `scripts/run-flow.mjs` and watch it with `scripts/flow-heartbeat-monitor.mjs` (one monitor
Expand Down
4 changes: 2 additions & 2 deletions docs/production-bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ before the first node-owner link in Stage 10 and operator `createlink` calls in
| `compute_bps` | `4000` | 40% → producers + batch ops |
| `capex_bps` | `2000` | 20% → `sysio.ops` |
| `governance_bps` | `1000` | 10% → `sysio.gov` |
| *(implicit capital reserve)* | `3000` | `10000 − compute − capex − governance`; stays on `sysio`, drained by `fundclaim` |
| *(implicit capital reserve)* | `3000` | `10000 − compute − capex − governance`; stays on `sysio`; no automatic LIQ or DClaim capital draw |
| `producer_bps` | `7000` | compute split: 70% producers |
| `batch_op_bps` | `3000` | compute split: 30% batch ops |
| `standby_end_rank` | `28` | producers ranked ≤28 are standby-eligible |
Expand Down Expand Up @@ -444,7 +444,6 @@ contract/mint addresses via the same shapes.
registered liq token: `{sym:"9,LIQETH", chain_code:slug("ETHEREUM"), token_code:slug("LIQETH")}` and
`{sym:"9,LIQSOL", chain_code:slug("SOLANA"), token_code:slug("LIQSOL")}`. Syndicated outpost custody is
minted 1:1 into these symbols.
36. `sysio.liq::setkicker({bps:200})` — `[sysio.liq@active]` — the T5 yield kicker.
37. **Underwriting + syndication rules:**
- `sysio.bond::setconfig({hold_bps:1000})` — `[sysio.bond@active]` — the hold bond, in bps of a request's
covered amount (the contract default, set explicitly).
Expand All @@ -459,6 +458,7 @@ contract/mint addresses via the same shapes.
| `window_sec` | `60` | challenge window before `sysio.bond::approve` can succeed **(cluster; production: longer)** |
| `bounty` | `0` | bounty posted on each envelope's `sysio.bond` request |
| `challenge_extra` | `1000000000` | one token charged to a challenger on top of the hold bond (the contract refuses `0`) |
| `min_desyndicate` | `1000000` | required gross return floor: 0.001 at nine decimals, shared by both pairs; an arbitrary launch choice based on 0.001 SOL |

The tooling then verifies that every shadow symbol is at the depot frame's precision and that each
`(chain, token)` pair is an active `TOKEN_KIND_LIQ` token with an active binding (otherwise `sysio.msgch`
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"@types/node": "25.5.0",
"@types/proper-lockfile": "^4.1.4",
"@wireio/opp-typescript-models": "^1.0.51",
"@wireio/sdk-core": "^1.0.96",
"@wireio/sdk-core": "^1.0.97",
"@wireio/shared": "^1.0.96",
"@wireio/shared-node": "^1.0.96",
"eslint": "^10.7.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/cluster-tool-shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"dependencies": {
"@3fv/prelude-ts": "^0.8.42",
"@wireio/opp-typescript-models": "^1.0.51",
"@wireio/sdk-core": "^1.0.96",
"@wireio/sdk-core": "^1.0.97",
"@wireio/shared": "^1.0.96",
"zod": "^4.4.3"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe("ExternalOutpostConfig", () => {
const config: ExternalOutpostConfig = {
ethereum: {
addressFile: "outpost-addrs.json",
abiFiles: ["eth-abis/OPP.json", "eth-abis/OperatorRegistry.json"],
abiFiles: ["eth-abis/OPP.json", "eth-abis/OPPInbound.json"],
chainId: 1
},
solana: { idlFile: "solana-idls/opp_outpost.json" }
Expand Down
3 changes: 1 addition & 2 deletions packages/cluster-tool/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,8 +384,7 @@ A flow is a `FlowScenario` composed onto the same engine via
`flow-emergency-stop`, `flow-emissions-soak`, `flow-liq-syndication`,
`flow-liq-yield`, `flow-node-owner-nft`, `flow-operator-collateral-deposit`,
`flow-producer-registration`, `flow-syndication-challenge`,
`flow-syndication-rate-limit`, `flow-syndication-underwriting`,
`flow-yield-distribution` — for end-to-end examples.
`flow-syndication-rate-limit`, `flow-syndication-underwriting` — for end-to-end examples.

---

Expand Down
2 changes: 1 addition & 1 deletion packages/cluster-tool/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"@wireio/debugging-server": "workspace:*",
"@wireio/debugging-shared": "workspace:*",
"@wireio/opp-typescript-models": "^1.0.51",
"@wireio/sdk-core": "^1.0.96",
"@wireio/sdk-core": "^1.0.97",
"@wireio/shared": "^1.0.96",
"@wireio/shared-node": "^1.0.96",
"archiver": "^8.0.0",
Expand Down
49 changes: 35 additions & 14 deletions packages/cluster-tool/src/flow/SyndicationScenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,36 @@ export namespace SyndicationScenario {
Assert.ok(row, "missing scenario envelope")
return row
}
/**
* The decoded `SyndicateLIQ` attestations the Solana outpost relayed to the depot for `user`'s
* wallet and `amount`, read from the cluster's OPP debugging artifacts.
*
* @param ctx - The build context whose cluster path holds the artifacts.
* @param user - The syndicating user, identified by their persisted wallet.
* @param amount - The syndicated amount, in base units.
* @return Every matching attestation; empty until the debugging plugin has persisted the
* envelope that carries one.
*/
export async function readDecodedSyndications(
ctx: ClusterBuildContext,
user: User,
amount: bigint
): Promise<SyndicateLIQ[]> {
const wallet = publicKey(ctx, user)
return (
await readEnvelopeAttestations(
oppDebuggingPath(ctx.config.clusterPath),
DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT,
AttestationType.SYNDICATE_LIQ
)
)
.map(bytes => SyndicateLIQ.fromBinary(bytes))
.filter(
message =>
message.amount?.amount === amount &&
Buffer.from(message.user?.address).toString("hex") === wallet
)
}
/** Wait for intake, prove it is held and capture the consensus epoch and request. */
export function planVerifyIntake(
actor: Report.Actor,
Expand All @@ -387,6 +417,7 @@ export namespace SyndicationScenario {
name,
description,
async ctx => {
let matching: SyndicateLIQ[] = []
await pollUntil(
name,
async () => {
Expand All @@ -399,27 +430,17 @@ export namespace SyndicationScenario {
publicKey(ctx, user)
)
if (!envelope) return false
// The debugging plugin persists the relayed envelope after the depot accepts it, so
// the decoded attestation can trail the held row; wait for both.
matching = await readDecodedSyndications(ctx, user, amount)
if (matching.length === 0) return false
ctx.outputs.set(epoch, envelope.epoch_index)
ctx.outputs.set(request, envelope.request_id)
return true
},
ProtocolTiming.SingleHopBudgetMs,
PollMs
)
const messages = (
await readEnvelopeAttestations(
oppDebuggingPath(ctx.config.clusterPath),
DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT,
AttestationType.SYNDICATE_LIQ
)
).map(bytes => SyndicateLIQ.fromBinary(bytes)),
matching = messages.filter(
message =>
message.amount?.amount === amount &&
Buffer.from(message.user?.address).toString("hex") ===
publicKey(ctx, user)
)
Assert.ok(matching.length > 0, "missing decoded syndication")
Assert.ok(
matching.every(message => message.totalSyndicated >= amount),
"custody attestation is below its own syndication"
Expand Down
27 changes: 3 additions & 24 deletions packages/cluster-tool/src/flow/oppEnvelopeScan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@ import {
* serialized OPP envelope bytes each side of a cross-chain edge emitted or
* consumed (file naming: `<epoch>-<DIRECTION>-<checksum>.data`).
*
* Negative-assertion flows (variance revert, private-reserve gating) use these
* to prove a `SWAP_REVERT` attestation circulated without pulling the full
* proto decoder into the scenario: the attestation's type tag has a fixed,
* unambiguous byte encoding inside the envelope payload.
* Flows use these to prove an attestation of a given type circulated without
* pulling the full proto decoder into the scenario: the attestation's type tag
* has a fixed, unambiguous byte encoding inside the envelope payload.
*/

/**
Expand Down Expand Up @@ -87,26 +86,6 @@ export function envelopeDataContains(
)
}

/**
* Whether a `SWAP_REVERT` attestation has circulated on `direction` — the
* canonical negative-path proof for the variance-revert and private-reserve
* gating flows.
*
* @param oppDebuggingDirectory - The cluster's `data/opp-debugging/` path.
* @param direction - The cross-chain edge to scan (default: depot → Ethereum).
* @returns Whether any matching envelope carries a SWAP_REVERT attestation.
*/
export function containsSwapRevert(
oppDebuggingDirectory: string,
direction: DebugOutpostEndpointsType = DebugOutpostEndpointsType.DEPOT_OUTPOST_ETHEREUM
): boolean {
return envelopeDataContains(
oppDebuggingDirectory,
direction,
attestationEntryTag(AttestationType.SWAP_REVERT)
)
}

/**
* Whether a `SYNDICATE_LIQ` attestation has circulated on `direction` — the
* outpost → depot proof that a syndication enqueued on the outpost reached an
Expand Down
21 changes: 1 addition & 20 deletions packages/cluster-tool/src/orchestration/ClusterBuildDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,6 @@ const ProducerSysGrant = "1000000.0000 SYS"
const WireSupply = "1000000000.000000000 WIRE"
/** WIRE's ABI symbol — the system token every swap pair quotes its second leg in. */
const WireSymbol = "9,WIRE"
/**
* `sysio.liq`'s T5 kicker: the share of every yield intake requested from T5
* and folded into the same index bump, in basis points. Mirrors the contract
* default, so a cluster pays yield the way the network will.
*/
const LiqKickerBps = 200
/**
* WIRE the panic account is funded with: none. It only signs `sysio.andon::pull`
* and `clear`, which move no funds; its resource policy pays for them.
Expand Down Expand Up @@ -972,7 +966,7 @@ export namespace ClusterBuildDefaults {
)
// The shadow-liq system, in the order sysio.liq's README gives a deployment:
// the swap's governance + system token, one shadow symbol per registered liq
// token, the T5 kicker. Registry setup a real depot performs too — unconditional.
// token. Registry setup a real depot performs too — unconditional.
ClusterBuildPhase.create<C>(
prerequisites,
"SwapConfig",
Expand All @@ -998,19 +992,6 @@ export namespace ClusterBuildDefaults {
"Open the shadow liq symbols on sysio.liq",
{}
)
ClusterBuildPhase.create<C>(
prerequisites,
"LiqConfig",
"Configure sysio.liq"
).push(
Steps.contracts.sysio.liq.planSetkicker<C>(
Actor.Sysio,
"configure-liq-kicker",
"set the T5 yield kicker",
{},
{ bps: LiqKickerBps }
)
)
// Underwriting and syndication: sysio.bond's hold bond, each shadow pair's
// sysio.synd rules, and the verify of the syndication preconditions. A pair with
// no syndconfig row releases nothing, so a real depot configures it too —
Expand Down
22 changes: 10 additions & 12 deletions packages/cluster-tool/src/orchestration/steps/RegistrySteps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ export namespace RegistrySteps {
* contract refuses a challenge whose charge is zero, so this is never 0.
*/
export const SyndicationChallengeExtra = 1_000_000_000
/** Shared gross return floor: 0.001 SOL at nine decimals, also used for LIQETH. */
export const MinimumDesyndication = 1_000_000

/** Durable bonder identity; flows resolve its ED/EM keys from readMockSyndicationBonder. */
export const MockSyndicationBonderLabel = "mock-syndication-bonder"
Expand Down Expand Up @@ -474,7 +476,8 @@ export namespace RegistrySteps {
desynd_refill: SyndicationBucketSize,
window_sec: ProtocolTiming.SyndicationChallengeWindowSec,
bounty: SyndicationBounty,
challenge_extra: SyndicationChallengeExtra
challenge_extra: SyndicationChallengeExtra,
min_desyndicate: MinimumDesyndication
}))

/** Seed chains + tokens + chain-token bindings. */
Expand Down Expand Up @@ -548,20 +551,17 @@ export namespace RegistrySteps {
code: "ETHEREUM",
outpost: {
opp_addr: ethAddress("OPP"),
opp_inbound_addr: ethAddress("OPPInbound"),
operator_registry_addr: "",
source_deposit_addr: ""
opp_inbound_addr: ethAddress("OPPInbound")
}
})
// One Solana program serves every role, so it goes in `opp_addr` alone —
// `sysio.chains` rejects an SVM row that fills the role-specific fields.
// One Solana program receives inbound and emits outbound envelopes, so it
// goes in `opp_addr` alone — `sysio.chains` rejects an SVM row that sets
// `opp_inbound_addr`.
await chains.actions.setoutpost.invoke({
code: "SOLANA",
outpost: {
opp_addr: artifacts.solanaProgramId,
opp_inbound_addr: "",
operator_registry_addr: "",
source_deposit_addr: ""
opp_inbound_addr: ""
}
})
}
Expand Down Expand Up @@ -751,9 +751,7 @@ export namespace RegistrySteps {
// daemons skip a chain whose addresses are not set yet.
const emptyOutpost: SysioContracts.SysioChainsOutpostAddrsType = {
opp_addr: "",
opp_inbound_addr: "",
operator_registry_addr: "",
source_deposit_addr: ""
opp_inbound_addr: ""
}
const chainRegistrations: SysioContracts.SysioChainsRegchainAction[] = [
{
Expand Down
Loading
Loading