This document describes how protocol fees are configured, how they are calculated, and how the treasury address participates in fee collection.
Both the global protocol contract and the payments contract store a fee_bps value:
fee_bpsis an unsigned 32-bit integer interpreted as basis points.MAX_BPS = 10_000represents 100%.- A
fee_bpsvalue of100therefore represents a 1% fee (100 / 10_000).
The shared helper require_valid_bps in crates/lily-common/src/lib.rs rejects any value greater than MAX_BPS, ensuring the fee can never exceed 100%.
The treasury Address is stored in:
contracts/protocol/src/lib.rsas protocol-wide configuration.contracts/payments/src/lib.rsas the settlement-specific treasury.
The treasury is the destination to which collected fees will be transferred when a payment intent is settled. Only the admin can update the treasury address via set_treasury.
Although the contracts currently configure fees, the actual transfer logic is intentionally left for a future settlement integration. When implemented, the expected behavior is:
- A payer creates a
PaymentIntentwith a grossamount. - Upon settlement, the gross amount is split into:
fee_amount = (amount * fee_bps) / MAX_BPSnet_amount = amount - fee_amount
fee_amountis credited to the treasury.net_amountis credited to the payee.
This keeps the fee calculation on-chain transparent and deterministic.
Fee calculations use integer arithmetic. The protocol uses floor rounding:
let fee_amount = (amount * fee_bps) / MAX_BPS;This means the fee is rounded down to the smallest representable unit of the settlement asset. The protocol absorbs any rounding residue rather than overcharging the payer.
| Gross amount | fee_bps | Fee calculation | Fee charged | Net to payee |
|---|---|---|---|---|
| 1_000_000 | 100 (1%) | (1_000_000 * 100) / 10_000 |
10_000 | 990_000 |
| 1_000_000 | 50 (0.5%) | (1_000_000 * 50) / 10_000 |
5_000 | 995_000 |
| 100 | 30 (0.3%) | (100 * 30) / 10_000 |
0 | 100 |
| 10_000 | 1 (0.01%) | (10_000 * 1) / 10_000 |
1 | 9_999 |
fee_bps = 0results in no fee:fee_amount = 0.fee_bps = MAX_BPS(10_000) results in the entire amount being taken as a fee:net_amount = 0.- Values outside the
0..=MAX_BPSrange are rejected at configuration time byrequire_valid_bps.
- The protocol admin calls
set_fee_bpson theprotocolcontract to change the global fee. - The protocol admin calls
set_fee_bpson thepaymentscontract to change the settlement-specific fee. - Each successful update emits a
("fee", admin)event carrying the newfee_bpsvalue.
- Whether fees are calculated once at settlement or cached in the intent record.
- Whether the treasury receives the fee as the same settlement asset or through a separate conversion path.
- Whether partial settlement or refund paths also apply fees.
These decisions will be documented in the settlement integration design when that work is undertaken.