Skip to main content

Fees

Two kinds of protocol fees: borrow interest and redemption fees. They have very different destinations.

This page traces both. User views of the staking destinations are in Earning.

The whole flow​

The Normal operation view is the everyday flow: each branch's FeeRouter splits borrow interest between its local Stability Pool and GlobalFeeRouter, which fans out to keeper rewards, LP staking, and FEE staking. Widths sit at the allocation PIs' 50% bias points; in live operation each split wanders inside its [10%, 60%] bounds.

The Bad-debt repair view shows the temporary diversion while a branch is undercollateralized: UNDERCOLLATERALIZED_FRAC = 50% of the FEE-staker allocation, plus redemption fees converted to RD, are routed to the sink branch's payDebt(...). The LP share is computed first and paid in full — only FEE stakers are diluted during repair. Once the sink branch is healthy again, normal flow resumes.

Redemption fees do not enter this diagram in normal operation; they are retained by the redeemed trove (see "Redemption-fee path" below). The only exception is the bad-debt-sink case, in which the redemption fee feeds the dotted path along with the staker diversion.

What gets charged, and where it goes​

FeeCharged inDestination
Borrow interestRDAccrued continuously via InterestEngine.drip() into trove debt. Routed through FeeRouter and GlobalFeeRouter to Stability Pool, LP staking, FEE staking, keeper rewards, and (when active) bad-debt repayment.
Redemption feeBranch collateralRetained by the redeemed trove as residual collateral. Does not flow to the routers; does not reach FEE stakers. The only exception is the bad-debt sink case (see below).

There is no upfront borrow fee (Liquity v1 had one; this protocol does not). The borrow rate accrues continuously instead.

Borrow-interest path​

This is the path that produces staker income.

Stage 1: branch FeeRouter​

Each branch has a FeeRouter. When the branch's InterestEngine accrues interest, the branch calls into the router. The router:

  1. Sends a fraction to the branch's local StabilityPool (folded into deposit balances, growing depositor RD without their action).
  2. Sends the rest to the singleton GlobalFeeRouter.

The SP split adapts to SP utilization:

SP_utilization   = stability_pool_balance / branch_debt
target = 25%
deadband = 5%
allocation_bound = [10%, 60%]
bias = 50%
half-life of EMA = 72 hours

If SP utilization is above target (the pool is large relative to debt; depositors are over-supplied), the router routes less to the SP and more to global stakers. If SP utilization is below target (the pool is thin; risk of unhandled liquidation), the router routes more to the SP, growing depositor balances faster and incentivizing fresh deposits.

The PI gains:

kp           = 0.5
timeConstant = 3 × half-life = 9 days
ki = kp / timeConstant

Slow but steady. The router does not whipsaw the SP split day-to-day. Initial spAllocFrac output is the 50% bias.

60%50%10%target 25% · ±5% deadband→ GlobalFeeRouterLP · FEE stakers · keeper→ Stability Pooldepositor yieldthin pool → 60%over-supplied → 10%0%25%50%SP utilization = SP balance ÷ branch debt →
Every branch's FeeRouter splits accrued interest between its local Stability Pool and the singleton GlobalFeeRouter, using a PI controller on a 72-hour EMA of SP utilization = SP balance ÷ branch debt. Inside the ±5% deadband around the 25% target it holds at the 50% bias; outside it ramps the SP share toward 60% (thin pool) or 10% (over-supplied), and the complement flows on to LP staking, FEE staking, and keeper rewards.

Stage 2: GlobalFeeRouter​

The singleton GlobalFeeRouter receives the cross-branch share. It splits across:

  1. Keeper rewards. First call. The router tops up keeperRewards toward a target balance.
  2. LP staking. The BPT stakers' share, lpAllocFrac of the total, controlled via PI on LP utilization. Paid in full (capped only by whatever the keeper top-up leaves).
  3. FEE staking. The remainder, to FEE stakers (across all tiers), as RD.
  4. Bad-debt repayment. If a branch has been marked currentUnderCollateralizedTM, half of the FEE-staker share is diverted to clear that branch's debt before distribution. The LP share is not touched.

Keeper rewards​

keeperRewardsTargetBalance = 1,000 RD
keeperRewardsMinBalance = 500 RD

If keeperRewards RD balance is below targetBalance, the router routes funds there until restored. The KeeperRewards contract is what pays the per-second drips and oracle update rewards (the par-update reward is capped at UPDATE_MAX_REWARD = 10 RD; the drip reward is a cubic ramp sized from the KeeperRewards balance, not a fixed cap).

LP vs FEE split​

The same PI-controlled allocation pattern, with different targets:

target_LP_utilization = 8%
deadband = 1%
allocation_bound = [10%, 60%]
bias = 50%
kp = 0.2
ki = kp / (3 × 72h)

LP_utilization = marketOracle_pool_liquidity_TWAL / RD_total_supply — the lower of the oracle's long and short liquidity TWALs, divided by rdToken.totalSupply(). The protocol wants roughly 8% of RD supply held as liquidity in the RD Balancer pool. If it's lower, LP gets more incentive; if higher, LP gets less.

60%50%10%target 8% · ±1% deadband→ FEE stakersremainder, as RD→ LP stakingBPT stakers, as RDthin LP → 60%deep LP → 10%0%8%16%LP utilization = RD pool liquidity (TWAL) ÷ RD supply →
After the keeper top-up, the GlobalFeeRouter splits what's left between LP staking and FEE staking using a PI controller on a 72-hour EMA of LP utilization = RD pool liquidity ÷ RD total supply. Inside the ±1% deadband around the 8% target it holds at the 50% bias; outside it ramps the LP share toward 60% (thin LP) or 10% (deep LP), and the complement goes to FEE stakers.

The other half goes to FEE stakers, distributed across the three tiers in proportion to stake × tier_multiplier. See FEE staking.

Bad-debt repayment​

UNDERCOLLATERALIZED_FRAC = 50%

When a branch has bad debt, 50% of the FEE-staker share (availForDebt = st × UNDERCOLLATERALIZED_FRAC) is offered to payDebt(...) on the sink branch; whatever payDebt actually consumes is deducted, and FEE stakers receive the rest. The LP allocation is computed and paid before the diversion and is never diluted (GlobalFeeRouter._distributePending).

This is the autonomous solvency-restoration mechanism. No admin top-up needed. The protocol slowly bleeds fees into bad-debt repair until the sink branch is healthy again.

Redemption-fee path​

Redemption fees don't flow through the routers at all. From RedemptionLot.sol:

collateralFee is "Fee retained by the trove."

Mechanically:

gross_collateral_drawn = (redeemed_rd / oracle_price) × par
fee_in_collateral = gross_collateral_drawn × redemption_fee
collateral_to_redeemer = gross_collateral_drawn − fee_in_collateral

The trove's debt decreases by the full redeemed_rd (paid down at par). The trove's collateral decreases by only collateral_to_redeemer. The fee portion stays inside the trove as residual collateral, raising the trove's post-redemption ICR.

Why this design: it rewards borrowers for staying above the redemption frontier. A borrower whose trove gets partially redeemed against ends up with less debt and an improved ICR, partially compensated for the inconvenience of being chosen.

Exception: bad-debt sink​

There is one path where the redemption fee does flow rather than being retained. When the Aggregator has designated a currentUnderCollateralizedTM (a sink branch with bad debt), every redemption on healthy branches:

  • Converts the fee that would have been retained by the redeemed trove into RD.
  • Routes that RD to the sink branch's payDebt(...) to chip away at the bad debt.

So in the bad-debt-sink case, the redemption fee funds protocol solvency repair. In the normal case, it stays with the borrower. Either way, it never flows to FEE stakers.

See Redemption mechanism for the full bad-debt flow.

Why two stages and two PI loops on the borrow path​

  • Per-branch SP utilization is local. A branch's SP can be over- or under-supplied independently. Putting the SP allocator on the branch's FeeRouter lets each branch tune its own SP incentives.
  • LP utilization is global. One canonical pool with one TWAP, one liquidity number. So the LP allocator lives on the singleton GlobalFeeRouter.
  • Decoupling. SP and LP have very different time constants. Branching them makes the controllers independently tunable without one's noise feeding into the other's loop.

Where to dig deeper​