Difficulty: Expert. Scope: multi-week epic. Contract + SDK + dashboard. Product-direction decision required.
Problem
There is no way for the protocol, or a split operator, to take a fee. amounts() divides the full payment among recipients with zero skim, and there is no fee recipient or fee state anywhere. A marketplace that wants a platform cut, or the protocol itself wanting sustainable revenue, has to build fee logic outside Tributary and defeat the "one transaction, everyone paid" model. This is a common, expected primitive for a payments rail, and it is entirely absent.
This one carries a product-direction decision: whether Tributary should have fees at all, at the protocol level, the split level, or both. The first deliverable is that decision, not code.
Proposal (to be validated by RFC)
An optional fee, off by default and backward compatible:
- A governed fee rate (basis points) and a fee recipient.
- On
pay/distribute, skim the fee before splitting the remainder among recipients, emitting the fee as its own event leg.
- Governance over the rate and recipient (should reuse the upgrade-authority / threshold-controller model, not a lone key), with a sane maximum cap enforced on-chain so governance cannot set an abusive rate.
Why this is hard
- Conservation changes: the invariant becomes
amount_in == fee + sum(recipient_amounts). All the dust and rounding rules must be re-derived so the fee plus the splits still reconstruct the input exactly, and the Kani/fuzzing invariants must be updated to match.
- Interaction surface: fees must compose correctly with nested splits (is the fee taken once at the top or at every level?), with escrow
deposit/distribute, and with pay_many.
- Governance and safety: on-chain rate cap, authority model, and making the fee genuinely optional (a zero rate must cost nothing and behave exactly as today).
- Transparency: payers and recipients must be able to see the fee before and after, in
preview_payout and in events.
Deliverables
- Product RFC and a maintainer decision on whether/where fees apply.
- If approved: fee state, governed setters with an on-chain cap, fee-aware
amounts/payout, event legs, and preview_payout that shows the fee.
- SDK and dashboard surfacing of the fee.
- Tests including conservation-with-fee, zero-rate equivalence to today, and nested-split behavior.
Acceptance criteria
- A maintainer decision on scope (no fee / protocol / per-split / both) is recorded before any implementation. If declined, the issue closes as "won't do" with the rationale captured, which is itself a valid outcome.
- If implemented:
amount_in == fee + sum(recipient_amounts) holds exactly, with updated invariant tests.
- Zero fee rate is behaviorally identical to today and costs no extra resources.
- The rate is governed with an enforced on-chain maximum; an abusive rate is impossible.
preview_payout and events expose the fee transparently.
Related
Depends on the governance/authority model from the upgradeability and threshold-controller epics. Must update the conservation invariants proven in the Kani and differential-fuzzing epics.
Difficulty: Expert. Scope: multi-week epic. Contract + SDK + dashboard. Product-direction decision required.
Problem
There is no way for the protocol, or a split operator, to take a fee.
amounts()divides the full payment among recipients with zero skim, and there is no fee recipient or fee state anywhere. A marketplace that wants a platform cut, or the protocol itself wanting sustainable revenue, has to build fee logic outside Tributary and defeat the "one transaction, everyone paid" model. This is a common, expected primitive for a payments rail, and it is entirely absent.This one carries a product-direction decision: whether Tributary should have fees at all, at the protocol level, the split level, or both. The first deliverable is that decision, not code.
Proposal (to be validated by RFC)
An optional fee, off by default and backward compatible:
pay/distribute, skim the fee before splitting the remainder among recipients, emitting the fee as its own event leg.Why this is hard
amount_in == fee + sum(recipient_amounts). All the dust and rounding rules must be re-derived so the fee plus the splits still reconstruct the input exactly, and the Kani/fuzzing invariants must be updated to match.deposit/distribute, and withpay_many.preview_payoutand in events.Deliverables
amounts/payout, event legs, andpreview_payoutthat shows the fee.Acceptance criteria
amount_in == fee + sum(recipient_amounts)holds exactly, with updated invariant tests.preview_payoutand events expose the fee transparently.Related
Depends on the governance/authority model from the upgradeability and threshold-controller epics. Must update the conservation invariants proven in the Kani and differential-fuzzing epics.