Incentivization

An LP’s claim on a leg is balance × liquidityIndexWad, so every on-chain way of paying BTR liquidity reduces to raising that index. Four levers do it; nothing else on chain does. Points, third-party reward campaigns and venue reward tokens are off-chain distribution the protocol carries no accounting for (§6). Every lever is per pool and per chain. “AC owner” below means the pool’s own seat (Pool.poolAdmin()): the protocol’s AccessControl.owner() on a protocol pool, the deployer’s seat on a white-label one. LP mechanics: Providing Liquidity.


1. The levers

LeverCallWhoEffect
Fee splitrequestOp(UPDATE_FEES) → executeUpdateFeesAC owner, TUNING tierLower protoSharePct → larger LP slice of every swap and flash spread
Direct subsidyPool.donate(token, amount)Anyone; deposit-lane holders only while DEPOSIT_GATED_BIT is setRaises one leg’s index, one-shot
POLPool.deposit(token, amount)AnyoneAdds depth; earns fees, pays no one else
Hook yieldYieldHook.rebalance() → Pool.hookCreditYieldKeeper or ownerVenue yield on the invested slice lands on the index

2. Fee split

protoSharePct is the percentage of the swap and flash spread routed to the pool’s fee sink; the remainder raises the LP index. Set in initdata at createPool, changed afterwards through the queue:

IPool.FeeParams memory p = IPool.FeeParams({protoSharePct: 0, flashFeePbps: 100, flowCooldownSecs: IPool(pool).flowCooldownSecs()}); admin.requestOp(pool, uint8(IPool.OpType.UPDATE_FEES), bytes32(0), abi.encode(p)); // … TUNING tier delay, 1 hour under PROD_DELAYS … admin.executeUpdateFees(pool); // emits FeeParamsUpdated

protoSharePct = 0 is a valid, permanent “all fees to LPs” launch stance and costs nothing on the hot path: the fee split returns the whole fee as lpFee. No token, no distributor, no claim.

Range is [0, 100]; anything above reverts InvalidInput. UPDATE_FEES is pool-wide, so its subject is bytes32(0). It is also the only way to lower flowCooldownSecs; Admin.setFlowCooldown only raises it. Receiving side: Protocol Fee Collection.


3. Donations

function donate(address token, uint256 amount) external payable;

Pulls amount, adds it to both reserves and liabilities, and raises that leg’s index. Emits Donated(sender, token, amount). Coverage is unchanged: a gift to existing holders of the leg, not new depth.

Two hard rules:

  1. A donation into an unopened leg is permanently unclaimable. With liabilities == 0 there is no share to raise; the surplus above the dead seed backs no claim. deposit first, then donate.
  2. It is instantaneous and un-vested. A donation lands on whoever holds the leg’s LPToken in that block, including someone who deposited one block earlier. Only the flow cooldown stands between a donation and a JIT deposit. Size and time donations accordingly, or pay through a hook instead.

4. Protocol-owned liquidity

POL is a plain deposit. It earns the LP slice like any other position and deepens the book, tightening quoted spread for everyone: that is its incentive value. It pays no one else directly.

LPToken is a standard ERC-20 whose minted quantity is frozen for Pool.flowCooldownSecs() (default 15 s, maximum 300). Transfers of unfrozen balance are unrestricted, which makes the receipt usable as a campaign measurement basis (§6). Mechanics: Flow Guards.


5. Hook yield

A YieldHook is the only lever here that pays LPs continuously. Full mechanics, buffer sizing and the dual ledger: Pool Hooks. Incentive-relevant facts:

  • sync() and rebalance() read venue NAV and book the gain via Pool.hookCreditYield, raising the index for every holder of that leg.
  • The credit is capped as a rate, not a per-call allowance: at most maxHarvestCreditBps of book per day, default 100 BPS/day, hard ceiling 500. A campaign that dumps a large one-shot gain into the venue is credited over days, not in one block. 0 disables crediting entirely. Details: Hooks §5.2.
  • Yield only accrues on what is invested, and the owner caps that: a hook starts at maxInvested 0, the position is held to 5% of the venue’s live cash, and no invest lands above 95% venue utilization (Hooks §5.4). On a thin venue the credit is small whatever the rate.
  • Venue reward tokens do not reach LPs. Two claims pull them onto the hook: claimMerkl for Merkl campaigns (the hook is the claimant, the distributor is a fixed address) and claimVenueIncentives for the venue’s own program (Aave, Venus). sweepIncentives always pushes them to pool.treasury(). Adapters never swap rewards. Routing that value back to LPs is a treasury decision, executed as a donate (§3).

Installing a hook is UPDATE_HOOK at the LISTING tier, 1 day.


6. Third-party campaigns

The protocol exposes no reward-distribution surface (no gauge, no notifyRewardAmount, no staking wrapper, no referral fee-share), so a campaign operator paying BTR LPs runs it entirely outside the protocol:

  1. Measure: snapshot LPToken.balanceOf per holder for the leg. One EIP-1167 clone per (pool, asset), address from Pool.lpToken(token), ABI as LP_TOKEN_ABI in @btr-protocol/sdk/abis. Balances are already index-adjusted claims, so no unit conversion is needed. Snapshot each chain separately.
  2. Distribute: the operator’s own Merkl campaign or distributor. BTR ships neither and takes no custody.

The one in-protocol alternative is to convert the campaign budget into a donate on the incentivized leg. Trustless, no claim UI, but untargeted: it pays every current holder pro rata, including passive ones.


7. End to end

Launch stance for an incentivized leg: all fees to LPs, a hook for baseline yield, and a periodic treasury donation funded by swept venue rewards.

import { encodeAbiParameters } from 'viem'; import { Contract } from '@btr-protocol/sdk/eth'; import { ADMIN_ABI, POOL_ABI } from '@btr-protocol/sdk/abis'; const admin = new Contract({ address: adminAddress, abi: ADMIN_ABI, provider, account: owner }); const pool = new Contract({ address: poolAddress, abi: POOL_ABI, provider, account: treasury }); // OpType.UPDATE_FEES = 7 in IPool.OpType. ZERO32 = 32 zero bytes: UPDATE_FEES is // pool-wide, so its subject is ignored. const UPDATE_FEES = 7; const ZERO32 = `0x${'00'.repeat(32)}`; const feeParams = encodeAbiParameters( [{ type: 'tuple', components: [{ type: 'uint8' }, { type: 'uint16' }, { type: 'uint16' }] }], [[0, 100, 15]], // protoSharePct = 0, flashFeePbps = 100, flowCooldownSecs = 15 (unchanged) ); // 1. All swap fees to LPs (TUNING tier, 1 hour). await admin.write('requestOp', [poolAddress, UPDATE_FEES, ZERO32, feeParams]); // … after the delay … await admin.write('executeUpdateFees', [poolAddress]); // 2. Seed the leg BEFORE anything is donated into it (§3, rule 1). await pool.write('deposit', [token, seedAmount]); // 3. Install the hook: UPDATE_HOOK, LISTING tier, 1 day, then raise its maxInvested // (requestCurve, queued). Thereafter the keeper's rebalance() credits venue yield // to the index automatically. // 4. Recycle swept venue rewards from the treasury as a donation. await pool.write('donate', [token, sweptBudget]);

Order matters: step 2 before any donate, and the hook’s HOOK_PRE_OUTFLOW flag must be set before capital can be deployed at all.


8. Boundaries

ActionAuthorityTiming
donateanyone (deposit-lane holders while DEPOSIT_GATED_BIT is set)immediate
deposit (POL)anyoneimmediate; flowCooldownSecs transfer lock
UPDATE_FEESAC ownerTUNING, 1 hour
UPDATE_HOOKAC ownerLISTING, 1 day
rebalance, claimMerkl, claimVenueIncentives, sweepIncentiveskeeper or ownerimmediate
treasury rotation (the reward sink)AC ownerGOVERNANCE, 7 days
claimTreasurypool.treasury(), minting to itselfimmediate

No incentive lever can move reserves out of a pool. donate and deposit only add, hookCreditYield only raises the index, and the sweep moves reward tokens that were never pool reserves.