import logging import math import matplotlib.pyplot as plt import pandas as pd import numpy as np log = logging.getLogger(__name__) # UNISWAP_GAS=0 # LMSR_GAS=0 UNISWAP_GAS=115_000 LMSR_GAS=150_000 ETH_PRICE=4000 UNISWAP_GAS_COST=UNISWAP_GAS*ETH_PRICE/1e9 LMSR_GAS_COST=LMSR_GAS*ETH_PRICE/1e9 print(f' LMSR gas: ${LMSR_GAS_COST:.2}') print(f'Uniswap gas: ${UNISWAP_GAS_COST:.2}') def lmsr_swap_amount_out( balances, amount_in, token_in_index, token_out_index, lp_fee, kappa, ): """ Compute the LMSR kernel fee-free amountOut for swapping `amount_in` of token `token_in_index` into token `token_out_index`, applying lp_fee to the input amount (i.e., amount_in_net = amount_in * (1 - lp_fee)). Uses native Python floats for performance and simplicity. Notes on congruence with PartyPool / LMSRStabilized: - The kernel formula implemented here matches the LMSR closed-form: amountOut = b * ln(1 + r0 * (1 - exp(-a / b))) where r0 = exp((q_i - q_j) / b) and b = kappa * S (S = sum balances). - lp_fee is applied to the input before the kernel (fee-on-input), matching PartyPool's placement. - This function uses continuous float arithmetic and does NOT emulate PartyPool's integer/unit conversions or rounding (floor/ceil) and ppm fee quantization. Parameters: - balances: iterable of per-token balances (numbers). These represent q_i in internal units. - amount_in: input amount supplied by swapper (before fees). - token_in_index: index of the input token i. - token_out_index: index of the output token j. - lp_fee: fractional LP fee applied to the input (e.g., 0.003). - kappa: liquidity parameter κ (must be positive, in same units as balances). Returns: - float: net amountOut (exclusive of fees) the swapper receives from the pool (capped at pool balance). """ # Normalize and validate inputs (convert to floats) try: q = [float(x) for x in balances] a_in = float(amount_in) lpf = float(lp_fee) k = float(kappa) except (TypeError, ValueError) as e: raise ValueError("Invalid numeric input") from e n = len(q) if not (0 <= token_in_index < n and 0 <= token_out_index < n): raise IndexError("token indices out of range") if a_in <= 0.0: return 0.0 if k <= 0.0: raise ValueError("kappa must be positive") # Size metric S = sum q_i S = sum(q) if S <= 0.0: raise ValueError("size metric (sum balances) must be positive") # b = kappa * S b = k * S if b <= 0.0: raise ValueError("computed b must be positive") # Apply LP fee on the input amount (kernel is fee-free; wrapper handles fees) if not (0.0 <= lpf < 1.0): raise ValueError("lp_fee must be in [0, 1)") a_net = a_in * (1.0 - lpf) if a_net <= 0.0: return 0.0 qi = q[token_in_index] qj = q[token_out_index] # Guard: output asset must have non-zero effective reserve if qj <= 0.0: # No available output to withdraw return 0.0 # Compute r0 = exp((q_j - q_i) / b) so small-trade out/in ≈ marginal price p_j/p_i try: r0 = math.exp((qj - qi) / b) except OverflowError: raise ArithmeticError("exponential overflow in r0 computation") # Compute a/b a_over_b = a_net / b # exp(-a/b) try: exp_neg = math.exp(-a_over_b) except OverflowError: # If exp would underflow/overflow, treat exp(-a/b) as 0 in extreme case exp_neg = 0.0 # inner = 1 + r0 * (1 - exp(-a/b)) inner = 1.0 + r0 * (1.0 - exp_neg) # If inner <= 0, cap to available balance qj if inner <= 0.0: return float(qj) # amountOut = b * ln(inner) try: ln_inner = math.log(inner) except (ValueError, OverflowError): # Numeric issue computing ln; be conservative and return 0 return 0.0 amount_out = b * ln_inner # Safety: non-positive output -> return zero if amount_out <= 0.0: return 0.0 # Cap output to pool's current balance qj (cannot withdraw more than available) if amount_out > qj: return float(qj) return float(amount_out) def lmsr_marginal_price(balances, base_index, quote_index, kappa): """ Compute the LMSR marginal price ratio p_quote / p_base for the given balances state. Formula: b = kappa * S, where S = sum(balances) price = exp((q_quote - q_base) / b) Parameters: - balances: iterable of per-token balances (q_i) - base_index: index of the base token - quote_index: index of the quote token - kappa: liquidity parameter κ (must be positive) Returns: - float: marginal price p_quote / p_base """ try: q = [float(x) for x in balances] k = float(kappa) except (TypeError, ValueError) as e: raise ValueError("Invalid numeric input") from e n = len(q) if not (0 <= base_index < n and 0 <= quote_index < n): raise IndexError("token indices out of range") if k <= 0.0: raise ValueError("kappa must be positive") S = sum(q) if S <= 0.0: raise ValueError("size metric (sum balances) must be positive") b = k * S if b <= 0.0: raise ValueError("computed b must be positive") return float(math.exp((q[quote_index] - q[base_index]) / b)) def compare(file, tvl, fee, kappa): d = pd.read_csv(file) d.columns = ['block', 'price0', 'price1', 'in0', 'out0', 'rate'] # New approach: derive bases from initial external balances (assuming equal-valued deposits) # This matches the Solidity implementation and eliminates the κ·ln(price) constraint p0 = float(d.iloc[0].price0) # Set external balances assuming equal values: if token0 = B0 and token1 = B1, # and they have equal value, then B0 * price0 = B1 * price1 = V (value per asset) # For simplicity, choose B0 such that the total value is tvl, then B1 = B0 * price0 total_value = float(tvl) # Since B0 * price0 + B1 = total_value and B1 = B0 * price0, we get: # B0 * price0 + B0 * price0 = total_value, so B0 = total_value / (2 * price0) B0 = total_value / (2.0 * p0) # external balance of token 0 B1 = B0 * p0 # external balance of token 1 (equal value) external_balances = [B0, B1] # Derive bases: set base_i = B_i so that q_i = B_i / base_i = 1.0 internally bases = [B0, B1] # Internal balances: q_i = external_balance_i / base_i ≈ 1.0 q0 = B0 / bases[0] # ≈ 1.0 q1 = B1 / bases[1] # ≈ 1.0 balances = [q0, q1] print(f"External balances: {external_balances}") print(f"Bases: {bases}") print(f"Internal balances: {balances}") # Convert external input amounts to internal for LMSR calculations, then convert results back X = np.geomspace(1, 1_000_000, 100) orig_price = lmsr_marginal_price(balances, 0, 1, kappa) # Convert X to internal amounts, compute swap, then convert back to external in_out = [] for amount_in_external in X: # Convert external input to internal units amount_in_internal = amount_in_external / bases[0] # input token 0 # Compute swap in internal units amount_out_internal = lmsr_swap_amount_out(balances, amount_in_internal, 0, 1, fee, kappa) # Convert output back to external units amount_out_external = amount_out_internal * bases[1] # output token 1 in_out.append((float(amount_in_external), float(amount_out_external))) print(f"Sample internal/external conversions: {in_out[:3]}") # Compute initial marginal price in external units # Internal price is exp((q1 - q0)/b), external price needs conversion by bases[1]/bases[0] orig_price_external = orig_price * (bases[1] / bases[0]) # Relative execution price deviation from the initial marginal price: # slippage = |(amount_out/amount_in)/orig_price_external - 1| eps = 1e-12 Y = [max(eps, abs((amount_out / amount_in) / orig_price_external - 1.0)) for amount_in, amount_out in in_out] plt.plot(X, Y, label=f'LMSR {fee:.2%} κ={kappa:.2g}', color='cornflowerblue') # Uniswap execution price deviation from its initial quoted price: # slippage = |(out/in)/initial_price - 1| uniswap_exec_price0 = d.out0 / d.in0 uniswap_slippage0 = (uniswap_exec_price0 / d.iloc[0].price0 - 1.0).abs().clip(lower=1e-12) uniswap_fee = round(uniswap_slippage0.iloc[0], 6) plt.plot(d.in0, uniswap_slippage0, label=f'Uniswap {uniswap_fee:.2%}', color='hotpink') # uniswap_slippage1 = |(out1/in1)/price1 - 1| # plt.plot(d.in1, (d.out1 / d.in1 / d.iloc[0].price1 - 1.0).abs().clip(lower=1e-12), label='CP1') # Interpolate Uniswap slippage to match LMSR x-coordinates interp_uniswap = np.interp(X, d.in0, uniswap_slippage0) mask = Y < interp_uniswap plt.fill_between(X, 0, 1, where=mask, alpha=0.2, color='green') plt.xscale('log') plt.yscale('log') plt.gca().xaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: '{:g}'.format(x))) plt.gca().yaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: '{:.2%}'.format(y))) plt.gca().set_ylim(top=.1) plt.xlabel('Input Amount') plt.ylabel('Slippage') plt.title('Pool Slippages') plt.grid(True) plt.legend() plt.show() def plot_kappa(): X = np.geomspace(1, 10_000_000, 100) for kappa in np.geomspace(0.01, 100, 8): balance0 = 1_000_000 # estimated from the production pool balances = [balance0, balance0] Y = [1 - lmsr_swap_amount_out(balances, float(amount_in), 0, 1, 0.000010, kappa) / amount_in for amount_in in X] plt.plot(X / balance0, Y, label=f'{kappa:f}') plt.xscale('log') plt.yscale('log') plt.gca().xaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: '{:.2f}'.format(10000*x))) plt.gca().yaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: '{:.2%}'.format(y))) plt.xlabel('Input Amount (basis points of initial balance)') plt.ylabel('Slippage') plt.title('Pool Slippages by Kappa') plt.grid(True) plt.legend() plt.show() if __name__ == '__main__': # compare('uni4_quotes/swap_results_block_23640998.csv') compare('uni4_quotes/ETH-USDC-30.csv', 53_000_000, 0.0025, 0.00025) # compare('uni4_quotes/ETH-USDC-30.csv', 100_000, 0.0025, 0.00025) # plot_kappa()