Skip to content

feat(flow-underwriter-slashing): the bonded Tier-1 challenge flow (WIRE-297) - #56

Merged
heifner merged 6 commits into
masterfrom
feat/underwriter-challenge
Aug 13, 2026
Merged

feat(flow-underwriter-slashing): the bonded Tier-1 challenge flow (WIRE-297)#56
heifner merged 6 commits into
masterfrom
feat/underwriter-challenge

Conversation

@heifner

@heifner heifner commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The live-cluster validation of the underwriter challenge system (Wire-Network/wire-sysio#549 + Wire-Network/wire-libraries-ts#61): a new flow-underwriter-slashing package plus one harness fix the validation surfaced.

The flow

Two identical ETH→SOL underwritten swaps produce two CONFIRMED commitments by the same winner, then both challenge verdicts run on the real pipeline — ordering is load-bearing (both swaps confirm before any challenge; the upheld verdict slashes the cluster's only underwriter):

  1. ChallengeRejected (commitment B) first: openuwchal escrows the bond (asserted to the unit against the challenger's balance), three Tier-1 ballots REJECT_FORFEIT, chkuwchal resolves REJECTED_FORFEIT — the bond lands exactly on the wrongly-challenged underwriter, both locks persist with their holds cleared, the operator stays ACTIVE, the uwreq stays CONFIRMED.
  2. ChallengeUpheld (commitment A) last: three UPHOLD ballots — the operator flips SLASHED, both locks sweep through the deferred-slash branch, the uwreq finalizes COMPLETED, the bond refunds the challenger to its pre-filing balance, and sysio.chalg ends with zero WIRE custody.

The electorate is three roa::forcereg'd Tier-1 voters + the bootstrap's one Tier-1 owner (N=4, Q=3); openuwchal asserts the snapshotted quorum stays reachable by the flow's voters so a bootstrap-roster change fails loudly instead of deadlocking the vote. Commitments are distinguished by excluding already-captured uwreq ids (direction alone is ambiguous with two same-direction swaps).

Validated green: 154/154 Report steps on a live local cluster.

Harness fix the validation surfaced

  • fix(flow-underwriter-slashing): verdict matched by proto spelling — the uwchals verdict/ballot/reason cells ride the ABI enums extension, so get_table_rows serializes them as NAME spellings ("REJECTED_FORFEIT"); the poll now uses matchesProtoEnum (numeric, numeric-string, and spelling forms). Attempt-one of the live run proved the chain resolves + pays the forfeit inline while a Number() comparison stays blind forever.

(The branch briefly carried a create_reserve_spl_authority account rename in the SOL bootstrapper, reverted in d0229287: liqsol-core named that account authority_ata only in a Jul-9→Jul-29 window that a stale local wire-solana build happened to capture; current next (post-#407 GlobalConfig admin gating) uses admin_ata, which master's harness already targets.)

Merge order: Wire-Network/wire-sysio#549Wire-Network/wire-libraries-ts#61 → this. The e2e gate must run with all three BRANCH_*=feat/underwriter-challenge overrides.

heifner added 4 commits August 6, 2026 08:49
The regenerated SysioContractTypes (WIRE-297) makes lock_entry.challenge_id
a required field — the underwriter-challenge hold marker, 0 when unheld.
The zero-defaults lockRow fixture now supplies it, exactly the drift the
regen chain exists to catch. The flow package lands separately.

Change-Id: I3c0cfc3af42cc6a860f14da4edf69ffd00cefaf8
…RE-297)

Two identical ETH→SOL underwritten swaps produce two CONFIRMED commitments
by the same winner, then the flow exercises both challenge verdicts on the
live pipeline — ordering is load-bearing (both swaps confirm before any
challenge; the upheld verdict slashes the cluster's only underwriter):

- ChallengeRejected (commitment B) runs first: openuwchal escrows the bond
  (asserted to the unit against the challenger's balance), three Tier-1
  ballots REJECT_FORFEIT, chkuwchal resolves REJECTED_FORFEIT — the bond
  lands exactly on the wrongly-challenged underwriter, both locks persist
  with their holds cleared, the operator stays ACTIVE, the uwreq stays
  CONFIRMED.
- ChallengeUpheld (commitment A) runs last: three UPHOLD ballots, chkuwchal
  resolves UPHELD — the operator flips SLASHED, both locks sweep through the
  deferred-slash branch, the uwreq finalizes COMPLETED, the bond refunds the
  challenger to its pre-filing balance, and sysio.chalg ends with zero WIRE
  custody.

The electorate is three roa::forcereg'd Tier-1 voters + the bootstrap's one
Tier-1 owner (N=4, Q=3); openuwchal asserts the snapshotted quorum stays
reachable by the flow's voters so a bootstrap-roster change fails loudly
instead of deadlocking the vote. The swap substrate mirrors
flow-swap-with-underwriting's Phase A; commitments are distinguished by
excluding already-captured uwreq ids (direction alone is ambiguous with two
same-direction swaps).

Change-Id: I7c7fabdeb5d19448c8c33de7419d92833b3932c0
liqsol-core's clean-room OPP outpost (wire-solana 6e2fc70a) names the
funding token account `authority_ata`; the SPL reserve provisioning still
passed `adminAta`, so Anchor's account resolver failed the transaction
build with "Account `authorityAta` not provided" and the SOL outpost
deploy died before submitting anything. Surfaced by any cluster bootstrap
against a wire-solana tree built after the clean-room commit.

Change-Id: Ie22a3df710439d2dd13643b130cb013f6b33f5f5
The uwchals verdict/ballot/reason cells ride the ABI `enums` extension,
so get_table_rows serializes them as NAME spellings ("REJECTED_FORFEIT"),
not numbers — `Number(challenge.verdict)` was NaN forever and the verdict
poll timed out 420s AFTER the chain had already resolved the tally and
paid the forfeit inline (verified from the chkuwchal trace's inline
sysio.chalg → winner transfer of the exact bond). awaitVerdict now matches
via matchesProtoEnum, which covers the numeric, numeric-string, and
spelling forms.

Change-Id: Ic377d82b7644789bc0288e6b801b105c490ac859
…thority"

Reverts 714b68f. The fix matched a STALE local wire-solana tree: liqsol-core
named the account `authority_ata` only in the Jul-9 → Jul-29 window; the
GlobalConfig admin gating (ef70e254, in next via the Aug-5 develop merge #407)
renamed it `admin_ata`, which is what master's `adminAta` already targets.
The e2e gate (run 31127327308) failed all 14 flows on
"Account `adminAta` not provided" with the fix in place — current next wants
master's spelling. Local clusters must build wire-solana@next (post-#407)
instead.

Change-Id: I3b131ded7fdd56bc43b0a32ac89bf67b1ae36bad
@heifner

heifner commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

✅ e2e gate GREEN — all 14 flows passed

Run 31132787179 (BRANCH_WIRE_SYSIO + BRANCH_WIRE_LIBRARIES_TS + BRANCH_WIRE_TOOLS_TS = feat/underwriter-challenge; wire-ethereum/wire-solana at manifest next pins; Release build; full dynamic flow discovery — the new flow was picked up automatically):

  • ✅ batch-operator-slashing (328s)
  • ✅ batch-operator-termination (858s)
  • ✅ emissions-soak (2099s)
  • ✅ node-owner-nft (420s)
  • ✅ operator-collateral-deposit (693s)
  • ✅ reserve-lifecycle (811s)
  • ✅ swap-from-wire (597s)
  • ✅ swap-non-native-tokens (2115s)
  • ✅ swap-private-reserves (1694s)
  • ✅ swap-to-wire (540s)
  • ✅ swap-variance-revert (359s)
  • ✅ swap-with-underwriting (946s)
  • underwriter-slashing (795s) ← the new WIRE-297 flow
  • ✅ yield-distribution (599s)

@heifner
heifner requested a review from jglanz August 7, 2026 12:56
…mbond

sysio.chalg::chkuwchal now credits a claimable balance instead of transferring the bond (wire-sysio #549 review round 2): the tally can run inline under the epoch tick, where sysio.token::transfer's require_recipient(to) would run the recipient's own code and let it abort epoch advancement.

Each verdict is therefore asserted twice. First that resolution moved no WIRE — the bondcredits row carries the whole bond and both balances are unchanged, which is the property the pull exists to buy. Then a claimbond step pulls it, and a second verify asserts delivery: the forfeit lands on the wrongly-challenged underwriter exactly, the refund makes the challenger whole. The zero-custody assertion moves to the end, where it now means both bonds resolved AND claimed.

planClaimbond takes either a literal account (the challenger) or the commitment whose winner is owed the forfeit, resolved from ctx.outputs at run time; its runner asserts the credit exists before the pull, that the payout equals the credit, and that the row is erased. readBondCredit reads sysio.chalg::bondcredits.
@heifner
heifner merged commit 16ed06a into master Aug 13, 2026
@heifner
heifner deleted the feat/underwriter-challenge branch August 13, 2026 15:38
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