Skip to content

feat(fees): mirror the swap-fee split and cover it end-to-end - #54

Open
heifner wants to merge 2 commits into
masterfrom
feat/swap-fee-distribution
Open

feat(fees): mirror the swap-fee split and cover it end-to-end#54
heifner wants to merge 2 commits into
masterfrom
feat/swap-fee-distribution

Conversation

@heifner

@heifner heifner commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Harness + flow side of the swap-fee change that pays the winning underwriter and each participating reserve owner out of the WIRE leg.

Harness

  • WireReserveTool.splitWireFee mirrors sysio::opp::amm::split_wire_fee exactly — five shares (underwriter / rewards / emissions / src reserve / dst reserve), every rate off the same gross leg, exact conservation.
  • SwapScenarioContext.reserveOwnerFee reads a reserve's rate and earned amounts through the typed table accessor.
  • Steps.contracts.sysio.reserv gains planSetrsvfee / planClaimrsvfee (owner-signed — the owner isn't in the action data, so it rides the step input) and planSetconfig.
  • The bootstrap seeds sysio.reserv::setconfig with a zero emissions share, so no swap fee leaves custody at settlement by default.

Flow coverage

flow-swap-private-reserves was the right home: one account owns both private reserves, so a single swap charges it twice — the per-reserve rule gets exercised, not just asserted.

  • New ReserveOwnerFees phase — the owner prices the ETH reserve at 1% and the SOL reserve at 2%. Deliberately different rates, so a swapped or double-counted side can't hide behind equal numbers.
  • PhaseA / PhaseB now assert each reserve accrued its own rate off the same leg. PhaseB runs the inverse direction, which proves the rate follows the reserve rather than its source/destination role.
  • New ClaimReserveOwnerFees phase — owner claims each accrual; both sweep to zero, lifetime totals survive, and the owner's WIRE rises by exactly what the pair earned.
  • The swap-from-wire / swap-to-wire / with-underwriting custody assertions now expect only the drained rewards half to leave custody — the underwriter half stays as a uwfees accrual until claimed.

Verification

[flow-swap-private-reserves] SUCCEEDED against a live cluster — exit 0, every Report step ok, including all six new fee steps.

Against a 99,009,900 gross WIRE leg the on-chain split was exact:

Fee Rate On chain
Source reserve owner 100 bps 990,099
Destination reserve owner 200 bps 1,980,198
Underwriter 30 bps × 50% 148,514
Batch-op rewards remainder 148,515
Emissions 0 bps (default) 0

Destination gained precisely the 95,742,574 remainder. The one-subunit asymmetry between rewards and the underwriter is the remainder-taking design working — nothing leaks.

Also: pnpm build + pnpm lint clean; 125 tests pass across the touched suites.

Known gap

SwapScenarioContext.test.ts cannot execute on Node 22 — it imports the flow barrel → FlowCLIyargs@18 (ESM-only), and jest's require(ESM) needs Node ≥24.9. Its new reserveOwnerFee cases type-check but I have not seen them go green locally; they'll run in CI. This is the same pre-existing cause as the other env-blocked suites on this host, not something this PR introduces.

Note on the rebase

Branched off fresh origin/master, which already carried the SEC-129 uwreq lifecycle fix (PR #38). I had made the same fix independently while chasing a compile error from the regenerated types — that duplicate was dropped in favour of upstream's, and the tree was rebuilt, re-linted, and re-tested against the current base.


Related PRs — one change across four repos

Land in this order; the first three are coupled:

  1. feat(fees): pay underwriters and reserve owners out of the swap fee wire-sysio#546 — contracts: the fee split, accruals, and claim actions
  2. chore(sdk-core): regenerate SysioContractTypes for the swap-fee surface wire-libraries-ts#60 — regenerated SysioContractTypes from those ABIs
  3. feat(fees): mirror the swap-fee split and cover it end-to-end #54 — harness mirror + e2e flow coverage ← this PR
  4. Wire-Network/wire-platform-manifest#5 — bootstrap-doc corrections

wire-libraries-ts#60 must merge before wire-tools-ts#54 (the harness builds against the committed SysioContractTypes), and both need wire-sysio#546's ABIs first. The manifest PR is doc-only and independent.

Running the e2e gate on this set needs all three code branches passed explicitly:
BRANCH_WIRE_SYSIO=feat/swap-fee-distribution, BRANCH_WIRE_LIBRARIES_TS=feat/swap-fee-distribution, BRANCH_WIRE_TOOLS_TS=feat/swap-fee-distribution.

The tools-ts override is not optional — without it the gate discovers and runs master's flows, so none of the new fee coverage executes. wire-ethereum / wire-solana stay on their manifest defaults (next); neither repo is touched by this change.

Gate run: all 13 flows passed (2026-08-05) — resolved wire-sysio@f982725a, wire-libraries-ts@ab92a844, wire-tools-ts@74b2f092; wire-ethereum / wire-solana on manifest default next.

Follows the depot change that pays the winning underwriter and each
participating reserve owner out of the WIRE-leg fee.

Harness:
- WireReserveTool.splitWireFee mirrors sysio::opp::amm::split_wire_fee
  exactly — five shares (underwriter / rewards / emissions / src reserve /
  dst reserve), every rate off the same gross leg, exact conservation.
- SwapScenarioContext.reserveOwnerFee reads a reserve's rate and earned
  amounts through the typed table accessor.
- Steps.contracts.sysio.reserv gains planSetrsvfee / planClaimrsvfee
  (owner-signed; the owner is not in the action data, so it rides the step
  input) and planSetconfig.
- The bootstrap seeds sysio.reserv::setconfig with a zero emissions share,
  so no swap fee leaves custody at settlement by default.

Flows:
- flow-swap-private-reserves gains a ReserveOwnerFees phase (the owner
  prices both private reserves at 1% and 2%) and a ClaimReserveOwnerFees
  phase (owner claims each accrual). PhaseA/PhaseB now assert each reserve
  accrued its OWN rate off the same leg. One account owns both reserves,
  so a single swap pays it twice — which is the per-reserve rule under
  test rather than merely asserted. Two different rates are used so a
  swapped or double-counted side cannot hide behind equal numbers.
- The swap-from-wire / swap-to-wire / with-underwriting custody assertions
  now expect only the DRAINED rewards half to leave custody; the
  underwriter half stays as a uwfees accrual until claimed.

Verified: flow-swap-private-reserves SUCCEEDED against a live cluster.
Against a 99,009,900 gross leg the on-chain split was exact — source
reserve 990,099 (1%), destination 1,980,198 (2%), underwriter 148,514,
rewards 148,515, emissions 0 — with the destination gaining precisely the
95,742,574 remainder.

Change-Id: I4f2fe583973bf002e9e773f1cca16f8c48d5476c
… placeholder

`FromWireRevertFeeBps` mirrored the contract's `DEFAULT_FROMWIRE_REVERT_FEE_BPS`
— its JSDoc says so — but stayed at 10 when that default moved to 500 for the
launch value. Every cluster therefore priced from-WIRE revert churn at 0.1%
while the network will charge 5%.

Inert today: the fee is charged only on the two caller-fault `drainfwq`
branches (unpriceable target reserve, variance exceeded at drain), and no flow
drives either, so no flow's economics change. It mattered anyway — the harness
is the reference cluster, and a future flow touching that path would have
silently inherited the stale rate.

Nothing pinned the value. The contract's own `drainfwq_charges_revert_fee_on_
caller_fault` sets its rate via `setconfig` rather than reading the default, so
the drift was invisible to it too. Adds the missing bootstrap-payload
assertion, which walks the composed tree to the `UnderwriterConfig` phase's
`configure-uwrit` step — the only seam, since the payload is built inline in
`ClusterBuildDefaults.create`.

Change-Id: I111303c4389a3413c17412f85f26c6fec5626a30
@heifner
heifner requested a review from jglanz August 5, 2026 14:53
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.

1 participant