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:

fee_growth[i] += fee_amount[i] × PRECISION / r_int

where:

  • fee_amount[i] is the fee collected in token i
  • r_int is 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:

claimable[i] = positionR × (fee_growth[i] − checkpoint[i]) / PRECISION

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 claimable

Why O(1)?

Without this pattern, claiming fees would require:

  1. Iterating over all trades since the last claim
  2. Computing the position's share of each trade's fees
  3. Summing them up

This is O(T) where T is the number of trades — potentially millions.

With fee growth, claiming is:

  1. Read current fee_growth (n values)
  2. Subtract checkpoint (n subtractions)
  3. 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.r

This 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:

fee_growth[USDC] += 300,000 × 10⁹ / 1,000,000 = 3 × 10¹¹

An LP with 10,000 shares (1% of liquidity) can claim:

claim = 10,000 × 3 × 10¹¹ / 10⁹ = 3,000 USDC

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.