import logging import math from itertools import combinations log = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) # Now interpret the `weights` mapping as the desired pair fee (in bps) vs USDC. # Examples: # - USDC: 0.0 -> trading USDC<>USDC costs 0 bps # - USDT: 0.10 -> trading USDT<>USDC costs ~0.1 bps # - WIF: 30.0 -> trading WIF<>USDC costs ~30 bps # # For arbitrary pair A<>B we compose per-asset fees multiplicatively: # f_eff = 1 - (1 - f_A) * (1 - f_B) # which is ≈ f_A + f_B for small fees. STABLECOIN_RATE = 0.01 # bps X0 = STABLECOIN_RATE / 2 targets = { 'USDT': STABLECOIN_RATE, 'USDC': STABLECOIN_RATE, 'WBTC': 3.0, # ~3 bps vs USDC 'WETH': 5.0, # ~5 bps vs USDC 'WIF': 30.0, # ~30 bps vs USDC } weights = {k:v-X0 for k,v in targets.items()} # UI / safety parameters (bps) # cap_pair_fee_bps: global cap on the composed pair fee to avoid degenerate values (set to None to disable) cap_pair_fee_bps = None # safety cap (bps); adjust or set to None to disable # Compute geometric mean "base" of weights (kept for diagnostics) def geometric_mean(vals): # Guard: all weights must be positive (allow zero for USDC) for v in vals: if v < 0: raise ValueError("All weights must be non-negative") # exclude zeros when computing geometric mean to avoid exact zero product pos_vals = [v for v in vals if v > 0] if not pos_vals: return 0.0 return math.prod(pos_vals) ** (1.0 / len(pos_vals)) base = geometric_mean(list(weights.values())) ref_asset = 'USDC' # Convert UI bps -> fractional form for internal calculations def bps_to_frac(bps_val): return bps_val / 10_000.0 log.info( f"Fee model (weights are target bps vs USDC): ref_asset={ref_asset}, base_geo={base:.6f}" ) def clamp_nonneg(x): return x if x >= 0.0 else 0.0 def clamp_cap_frac(x_frac): """Clamp a fractional fee to configured cap_pair_fee_bps if set""" if cap_pair_fee_bps is None: return x_frac cap_frac = bps_to_frac(cap_pair_fee_bps) return min(x_frac, cap_frac) def per_asset_fee(w_bps): """ Interpret `w_bps` directly as the target bps vs USDC for that asset and return the fractional per-asset fee (clamped). So for asset A: f_A = bps_to_frac(weights[A]) """ return clamp_nonneg(bps_to_frac(w_bps)) def fee_for_pair(w_in_bps, w_out_bps): """ Compute effective fee fraction for a swap i->j using multiplicative composition of per-asset fees: f_i = per_asset_fee(w_in_bps) f_j = per_asset_fee(w_out_bps) f_eff = 1 - (1 - f_i) * (1 - f_j) Returns tuple (f_eff_frac, f_i_frac, f_j_frac). """ f_i = per_asset_fee(w_in_bps) f_j = per_asset_fee(w_out_bps) # multiplicative composition (accurate and gas-cheap counterpart) f_eff = 1.0 - (1.0 - f_i) * (1.0 - f_j) # apply global cap if configured f_eff = clamp_cap_frac(f_eff) return clamp_nonneg(f_eff), f_i, f_j def pct(x): return f"{100.0 * x:.6f}%" def bps(x): # x expected as fraction; convert to bps (1 bps = 1/10000) return f"{10_000.0 * x:.4f} bps" if __name__ == "__main__": names = list(weights.keys()) vals = list(weights.values()) # Warn if any single-asset implied fee would produce large numbers alert_threshold_bps = 1000.0 alerts = [] max_w = max(weights.values()) min_w = min(weights.values()) max_diff = max_w - min_w if max_w > alert_threshold_bps: alerts.append(("single_asset_too_large_bps", max_w)) if alerts: log.warning("Some fee configuration may produce large values (bps): %s", alerts) # Per-asset diagnostic: show per-asset fee (interpreted vs USDC) for n, w in weights.items(): f_token = per_asset_fee(w) print(f"{n:>12} {bps(f_token):>12}") print() for (name_a, w_a), (name_b, w_b) in combinations(weights.items(), 2): f_eff_ab, f_i_ab, f_j_ab = fee_for_pair(w_a, w_b) print( f"{name_a+'-'+name_b:>12} {bps(f_eff_ab):>12}" ) # -- Solidity (ABDK Q64.64) output -- # Print derived per-asset fees and cap as Solidity int128 Q64.64 constants that can be pasted # into a Solidity file using ABDK's 64.64 fixed-point representation (int128 constants). print("\n// Solidity (ABDK Q64.64) constants - generated by research/fees.py") print("// Per-asset fees (FEE_) are Q64.64 int128 literals representing the fractional fee") print("// Example: int128 internal constant FEE_USDT = 0x012345...; // comment\n") # Helper: order tokens consistently for array examples token_names = list(weights.keys()) for name in token_names: w_bps = weights[name] t_bps = targets[name] f_frac = per_asset_fee(w_bps) q64_int = int(round(f_frac * (1 << 64))) hexstr = hex(q64_int) print(f"int128 internal constant FEE_{name} = {hexstr}; // {bps(f_frac)}") # Cap constant if configured if cap_pair_fee_bps is not None: cap_frac = bps_to_frac(cap_pair_fee_bps) cap_q64 = int(round(cap_frac * (1 << 64))) print(f"int128 internal constant CAP_PAIR_FEE_Q64 = {hex(cap_q64)}; // cap {cap_pair_fee_bps} bps") # Example how to assemble into an array in the same order as `token_names`. print("\n// Example: build a per-asset fee array (same ordering as token_names list above)") print(f"// token order: {token_names}") print(f"int128[] memory perAssetFeesQ64 = new int128[]({len(token_names)});") for idx, name in enumerate(token_names): print(f"perAssetFeesQ64[{idx}] = FEE_{name};") print("\n// End of generated Solidity constants\n") # Notes: # - weights[name] should be set to the desired bps (not fraction) vs USDC. # - Per-asset fraction f_token = bps_to_frac(weights[name]) and # composed pair fee is f_eff = 1 - (1 - f_i) * (1 - f_j). # - This keeps USDC as the reference (weights['USDC'] == 0.0).