Fee Accounting
taurusSwap uses the fee-growth-per-unit-of-radiuspattern from Uniswap V3, adapted for Orbital's concentrated liquidity. This gives O(1) fee settlement regardless of how many trades happened.
The Pattern
On each swap, fees are accrued proportionally to each tick's liquidity:
where:
fee_amount[i]is the fee collected in token ir_intis the total interior radius (active liquidity)PRECISION = 10⁹is a fixed-point scaling factor
fee_growth[i]represents "fees earned per unit of liquidity" in token i. It's a global accumulator that only increases.
Position Checkpoints
Each LP position stores a checkpoint of fee_growth at the time of deposit (or last claim):
interface Position {
shares: bigint;
feeCheckpoints: bigint[]; // One per token
}When a position is created, the checkpoint is set to the currentfee_growthvalues. The position doesn't earn fees from trades that happened before it existed.
Claiming Fees
When an LP claims fees:
where positionRis the position's share of the tick's total liquidity.
@arc4.method
def claim_fees(
tick_id: arc4.UInt64,
) -> arc4.DynamicArray[arc4.UInt64]:
# Load position
pos = load_position(Txn.sender, tick_id)
# Load current fee_growth
fee_growth = load_fee_growth()
# Compute claimable per token
claimable = []
for i in range(n):
delta_growth = fee_growth[i] - pos.feeCheckpoints[i]
claim = pos.shares * delta_growth / PRECISION
claimable.append(claim)
# Update checkpoint to current
pos.feeCheckpoints = fee_growth
save_position(pos)
# Emit inner transfers
for i, amount in enumerate(claimable):
if amount > 0:
transfer_asset(token_asa[i], amount, Txn.sender)
return claimableWhy O(1)?
Without this pattern, claiming fees would require:
- Iterating over all trades since the last claim
- Computing the position's share of each trade's fees
- Summing them up
This is O(T) where T is the number of trades — potentially millions.
With fee growth, claiming is:
- Read current
fee_growth(n values) - Subtract checkpoint (n subtractions)
- Multiply by shares (n multiplications)
This is O(n) — constant time per claim, independent of trade count.
Fee Distribution Across Ticks
When a swap crosses multiple ticks, fees are distributed proportionally:
# For each tick that provided liquidity to this trade:
tick_fee_share = tick.r / total_liquidity_used
for i in range(n):
tick_fee_growth[i] += (fee_amount[i] * tick_fee_share) / tick.rThis ensures LPs who provided liquidity for a specific segment of the trade are compensated fairly.
Numerical Example
Suppose:
r_int = 1,000,000(1M liquidity)fee_bps = 30(0.3%)- Trade: 100M USDC in, 99.7M USDT out
- Fee: 0.3M USDC
Fee growth update:
An LP with 10,000 shares (1% of liquidity) can claim:
Precision and Overflow
PRECISION = 10⁹ was chosen to balance:
- Precision — Small positions should still earn measurable fees
- Overflow risk — fee_growth is a uint64; too large PRECISION risks overflow
At 10⁹, fee_growth can accumulate ~1.8 × 10¹⁰ before overflow — enough for trillions in volume.
Connection to SDK: The readPosition function returns both shares and pending fees (computed using this formula). Use it to show users their claimable balance before they sign.