-
Notifications
You must be signed in to change notification settings - Fork 11
feat(fees): pay underwriters and reserve owners out of the swap fee #546
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 5 commits
f982725
04a8e5d
6fbed1d
296c1ab
9665f78
cb31b67
9b8bddf
73da2da
8638612
baaace5
504cc7f
c55cc14
25092af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -172,38 +172,113 @@ inline uint64_t wire_to_token(uint64_t reserve_wire_amount, | |
| /// Basis-points denominator (10000 = 100%). | ||
| inline constexpr uint32_t BPS_TOTAL = 10000; | ||
|
|
||
| /// Decomposition of a swap fee taken out of the WIRE leg. | ||
| /// Decomposition of EVERY fee taken out of a swap's WIRE leg. | ||
| /// | ||
| /// Two independent fees ride the same leg (WIRE-281): the NETWORK fee | ||
| /// (`sysio.uwrit::fee_bps`, split between the winning underwriter, batch | ||
| /// operators and optionally the emissions treasury) and each participating | ||
| /// RESERVE's own owner fee. A chain-to-chain swap therefore pays three fees — | ||
| /// two reserve owners plus the network — while a WIRE-endpoint swap touches one | ||
| /// reserve and pays two. | ||
| struct wire_fee { | ||
| uint64_t fee = 0; ///< total fee charged (WIRE) | ||
| uint64_t reward_share = 0; ///< portion routed to the rewards bucket | ||
| uint64_t emissions_share = 0; ///< portion returned to the emissions treasury | ||
| uint64_t net = 0; ///< wire_amount - fee (continues through the swap) | ||
| uint64_t fee = 0; ///< TOTAL of every fee charged on the leg | ||
| uint64_t underwriter_share = 0; ///< network fee: the swap's winning underwriter | ||
| uint64_t reward_share = 0; ///< network fee: rewards bucket (batch operators) | ||
| uint64_t emissions_share = 0; ///< network fee: the `sysio` emissions treasury | ||
| uint64_t src_reserve_share = 0; ///< the SOURCE reserve owner's fee | ||
| uint64_t dst_reserve_share = 0; ///< the DESTINATION reserve owner's fee | ||
| uint64_t net = 0; ///< wire_amount - fee (continues through the swap) | ||
| }; | ||
|
|
||
| /// Split `wire_amount` into fee (`fee_bps`) + remainder, then split the fee into | ||
| /// a rewards share (`reward_share_bps`) and an emissions share (the rest). All | ||
| /// integer; `reward_share + emissions_share == fee` and `net + fee == | ||
| /// wire_amount` exactly (no rounding leak). Computed in `u128` to avoid overflow. | ||
| inline wire_fee split_wire_fee(uint64_t wire_amount, uint32_t fee_bps, uint32_t reward_share_bps) { | ||
| /// The ONE fee decomposition for a swap's WIRE leg — used by BOTH the read-only | ||
| /// quote and settlement, so the two can never drift by a rounding subunit. | ||
| /// | ||
| /// Every rate is applied to the SAME gross `wire_amount`, so the fees are purely | ||
| /// additive and order-independent — a user pays `fee_bps + src + dst` of the | ||
| /// leg, and no party's cut depends on who is computed first: | ||
| /// | ||
| /// * **Reserve fees** — `src_reserve_fee_bps` / `dst_reserve_fee_bps`, each | ||
| /// the owner fee of the reserve supplying that leg (0 for a WIRE endpoint, | ||
| /// which has no reserve). Accrue to those reserves' owners. | ||
| /// * **Network fee** — `fee_bps`, itself split in two stages: | ||
| /// 1. `underwriter_share_bps` of it goes to the winning underwriter; the | ||
| /// remainder is the **rewards pool**. | ||
| /// 2. `emissions_share_bps` of that POOL (not of the whole fee) goes to the | ||
| /// `sysio` emissions treasury; the rest is the batch-operator share. | ||
| /// | ||
| /// All integer, exact: the five shares sum to `fee` and `net + fee == | ||
| /// wire_amount`, because every stage takes a REMAINDER rather than a second | ||
| /// floored product. Computed in `u128` to avoid overflow. | ||
| /// | ||
| /// **Callers must check `net > 0`.** With enough stacked rates the total can | ||
| /// reach or exceed the leg. When it does, `fee` is CLAMPED to `wire_amount` and | ||
| /// `net` is 0 — so in THAT case alone the per-share fields sum to more than | ||
| /// `fee`, which is harmless because the caller rejects the swap and no share is | ||
| /// ever accrued. The settlement paths already assert this, and | ||
| /// `sysio.reserv::setrsvfee` / `sysio.uwrit::setconfig` cap each rate so a | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Medium] Treat zero-net as a valid configured rejection state. The paragraph first says stacked rates can consume the leg, then says the per-rate caps make that unrealistic, but valid settings can reach it directly—for example, network
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed, and your example is exact — fixed in I made it a test rather than only prose, since a comment is exactly what rotted here. New
Swept the matching claims: the One claim I deliberately kept, because sweeping it would have been wrong: You are also right about
|
||
| /// realistic combination cannot get there. | ||
| /// | ||
| /// Paths with no winning underwriter (a revert refund) pass 0 for | ||
| /// `underwriter_share_bps`, sending the whole network fee into the rewards pool. | ||
| /// The trailing rates default to 0, so a caller that charges only the network | ||
| /// fee — a revert, or a quote against a fee-free reserve — omits them. | ||
| inline wire_fee split_wire_fee(uint64_t wire_amount, | ||
| uint32_t fee_bps, | ||
| uint32_t underwriter_share_bps, | ||
| uint32_t emissions_share_bps = 0, | ||
| uint32_t src_reserve_fee_bps = 0, | ||
| uint32_t dst_reserve_fee_bps = 0) { | ||
| wire_fee r; | ||
| if (fee_bps > BPS_TOTAL) fee_bps = BPS_TOTAL; | ||
| if (reward_share_bps > BPS_TOTAL) reward_share_bps = BPS_TOTAL; | ||
| r.fee = static_cast<uint64_t>((static_cast<u128>(wire_amount) * fee_bps) / BPS_TOTAL); | ||
| r.reward_share = static_cast<uint64_t>((static_cast<u128>(r.fee) * reward_share_bps) / BPS_TOTAL); | ||
| r.emissions_share = r.fee - r.reward_share; | ||
| r.net = wire_amount - r.fee; | ||
| if (underwriter_share_bps > BPS_TOTAL) underwriter_share_bps = BPS_TOTAL; | ||
| if (emissions_share_bps > BPS_TOTAL) emissions_share_bps = BPS_TOTAL; | ||
| if (src_reserve_fee_bps > BPS_TOTAL) src_reserve_fee_bps = BPS_TOTAL; | ||
| if (dst_reserve_fee_bps > BPS_TOTAL) dst_reserve_fee_bps = BPS_TOTAL; | ||
|
|
||
| // Each reserve owner's cut, off the gross leg. | ||
| r.src_reserve_share = static_cast<uint64_t>((static_cast<u128>(wire_amount) * src_reserve_fee_bps) / BPS_TOTAL); | ||
| r.dst_reserve_share = static_cast<uint64_t>((static_cast<u128>(wire_amount) * dst_reserve_fee_bps) / BPS_TOTAL); | ||
|
|
||
| // The network fee, off the same gross leg, then its own two-stage split. | ||
| const uint64_t network_fee = static_cast<uint64_t>((static_cast<u128>(wire_amount) * fee_bps) / BPS_TOTAL); | ||
| r.underwriter_share = static_cast<uint64_t>((static_cast<u128>(network_fee) * underwriter_share_bps) / BPS_TOTAL); | ||
| const uint64_t rewards_pool = network_fee - r.underwriter_share; | ||
| r.emissions_share = static_cast<uint64_t>((static_cast<u128>(rewards_pool) * emissions_share_bps) / BPS_TOTAL); | ||
| r.reward_share = rewards_pool - r.emissions_share; | ||
|
|
||
| // Sum in u128. Three stacked rates carry past uint64 on an extreme leg, and a | ||
| // WRAPPED total is worse than a saturated one: it reports a small positive | ||
| // `net` for a swap that consumed the whole leg, so the caller's `net > 0` | ||
| // gate waves it through. Compare in u128 and narrow only when the total | ||
| // genuinely fits; otherwise clamp to the leg so a fully-consumed leg is | ||
| // reported as exactly that. | ||
| const u128 total_fee = static_cast<u128>(network_fee) | ||
| + static_cast<u128>(r.src_reserve_share) | ||
| + static_cast<u128>(r.dst_reserve_share); | ||
| if (total_fee >= static_cast<u128>(wire_amount)) { | ||
| r.fee = wire_amount; | ||
| r.net = 0; | ||
| } else { | ||
| r.fee = static_cast<uint64_t>(total_fee); | ||
| r.net = wire_amount - r.fee; | ||
| } | ||
| return r; | ||
| } | ||
|
|
||
| /// Post-fee swap quote along the depot curve. A WIRE endpoint (`src_is_wire` / | ||
| /// `dst_is_wire`) skips that side's reserve — the depot IS the WIRE side. For a | ||
| /// non-WIRE side pass that reserve's `(chain_amount, wire_amount, cw)`. The fee | ||
| /// (`fee_bps`) is charged on the WIRE leg. Returns the post-fee output the | ||
| /// recipient could receive, or 0 on degenerate input. Mirrors settlement | ||
| /// exactly so quotes and books agree. | ||
| /// non-WIRE side pass that reserve's `(chain_amount, wire_amount, cw)` AND its | ||
| /// `owner_fee_bps`; a WIRE endpoint has no reserve, so pass 0 for its fee. | ||
| /// | ||
| /// Every fee — the network `fee_bps` plus each participating reserve's owner fee | ||
| /// — is charged on the WIRE leg through the SAME `split_wire_fee` the settlement | ||
| /// paths use, so quotes and books agree by construction. Returns the post-fee | ||
| /// output the recipient could receive, or 0 on degenerate input (including a | ||
| /// stacked-fee combination that leaves nothing). | ||
| inline uint64_t quote_swap(bool src_is_wire, uint64_t src_chain, uint64_t src_wire, uint32_t src_cw, | ||
| bool dst_is_wire, uint64_t dst_chain, uint64_t dst_wire, uint32_t dst_cw, | ||
| uint64_t amount_in, uint32_t fee_bps) { | ||
| uint64_t amount_in, uint32_t fee_bps, | ||
| uint32_t src_reserve_fee_bps = 0, uint32_t dst_reserve_fee_bps = 0) { | ||
| if (amount_in == 0) return 0; | ||
| if (src_is_wire && dst_is_wire) return amount_in; // WIRE->WIRE is a plain transfer | ||
|
|
||
|
|
@@ -212,7 +287,13 @@ inline uint64_t quote_swap(bool src_is_wire, uint64_t src_chain, uint64_t src_wi | |
| : token_to_wire(src_chain, src_wire, src_cw, amount_in); | ||
| if (wire_leg == 0) return 0; | ||
|
|
||
| const uint64_t wire_net = split_wire_fee(wire_leg, fee_bps, /*reward_share_bps*/0).net; | ||
| // A WIRE endpoint has no reserve on that side and therefore charges no | ||
| // reserve fee, whatever the caller passed. | ||
| const uint64_t wire_net = split_wire_fee(wire_leg, fee_bps, /*underwriter_share_bps*/0, | ||
| /*emissions_share_bps*/0, | ||
| src_is_wire ? 0 : src_reserve_fee_bps, | ||
| dst_is_wire ? 0 : dst_reserve_fee_bps).net; | ||
| if (wire_net == 0) return 0; | ||
| if (dst_is_wire) return wire_net; // user receives WIRE directly | ||
| return wire_to_token(dst_wire, dst_chain, dst_cw, wire_net); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Low] Qualify conservation to the non-clamped path. This states unconditionally that the five share fields sum to
fee, but the paragraph below correctly says they can sum to more thanfeeaftertotal_feeis clamped towire_amount. Please state that five-share conservation holds only whentotal_fee < wire_amount;net + fee == wire_amountremains unconditional.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct — fixed in
25092af3b4. The sentence contradicted the clamp paragraph directly below it, which is the worse form of the error: a reader who trusts the first statement has no reason to read on, and the clamp is precisely the case where the five shares stop summing tofee.Split into the conditional and unconditional halves:
net + fee == wire_amountis called out as unconditional exactly as you say, since it holds on both paths — the clamp setsfee = wire_amount, net = 0, which satisfies it.Comment only. See my note on the other three in this batch regarding test verification.