diff --git a/src/IPartyPool.sol b/src/IPartyPool.sol index 29ea18d..2888f04 100644 --- a/src/IPartyPool.sol +++ b/src/IPartyPool.sol @@ -117,9 +117,9 @@ interface IPartyPool is IERC20Metadata, IOwnable { /// @notice Callable by anyone, sends any owed protocol fees to the protocol fee address. function collectProtocolFees() external; - /// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q) - /// @dev Pools are constructed with a κ value; this getter exposes the κ used by the pool. - function kappa() external view returns (int128); + /// @notice Fixed LMSR curvature parameter b (Q64.64) Larger values allow more liquidity to be taken with less price impact. + /// @dev Pools are constructed with a fixed b; this getter exposes the b used by the pool. + function bFixed() external view returns (int128); /// @notice If a security problem is found, the vault owner may call this function to permanently disable swap and /// mint functionality, leaving only burns (withdrawals) working. diff --git a/src/LMSRStabilized.sol b/src/LMSRStabilized.sol index a08f9f9..57c10bd 100644 --- a/src/LMSRStabilized.sol +++ b/src/LMSRStabilized.sol @@ -13,7 +13,7 @@ library LMSRStabilized { struct State { uint256 nAssets; - int128 kappa; // liquidity parameter κ (64.64 fixed point) + int128 bFixed; // fixed b curvature (Q64.64) int128[] qInternal; // cached internal balances in 64.64 fixed-point format } @@ -26,11 +26,10 @@ library LMSRStabilized { function init( State storage s, int128[] memory initialQInternal, - int128 kappa + int128 bFixed ) internal { s.nAssets = initialQInternal.length; - // Initialize qInternal cache if (s.qInternal.length != initialQInternal.length) { s.qInternal = new int128[](initialQInternal.length); } @@ -42,9 +41,73 @@ library LMSRStabilized { int128 total = _computeSizeMetric(s.qInternal); require(total > int128(0), "LMSR: total zero"); - // Set kappa directly (caller provides kappa) - s.kappa = kappa; - require(s.kappa > int128(0), "LMSR: kappa>0"); + require(bFixed > int128(0), "LMSR: b<=0"); + s.bFixed = bFixed; + } + + /* -------------------- + Virtual offset helpers (pure) for initialization + -------------------- */ + + /// @notice Compute per-asset virtual offsets v so that the initial marginal price between `base` and `quote` + /// equals `targetPrice` (Q64.64). Returns an array v with v_base and v_quote adjusted and others zero. + /// @dev Given reserves r, we want s = r + v with s_quote - s_base = b * ln(targetPrice). + /// Let deltaDesired = b*ln(targetPrice), deltaCurrent = r_quote - r_base, then + /// let adj = deltaDesired - deltaCurrent; choose v_base = -adj/2, v_quote = adj/2, others 0. + function computeOffsetsForPricePair( + int128 b, + int128[] memory reservesInternal, + uint256 baseIndex, + uint256 quoteIndex, + int128 targetPrice + ) internal pure returns (int128[] memory vOffsets) { + require(b > int128(0), "LMSR: b<=0"); + uint256 n = reservesInternal.length; + require(baseIndex < n && quoteIndex < n, "LMSR: idx"); + + vOffsets = new int128[](n); + + // adj = b * ln(targetPrice) - (r_quote - r_base) + int128 deltaDesired = b.mul(_ln(targetPrice)); + int128 deltaCurrent = reservesInternal[quoteIndex].sub(reservesInternal[baseIndex]); + int128 adj = deltaDesired.sub(deltaCurrent); + + int128 two = ABDKMath64x64.fromUInt(2); + // v_base = -adj/2; v_quote = adj/2 + vOffsets[baseIndex] = adj.neg().div(two); + vOffsets[quoteIndex] = adj.div(two); + return vOffsets; + } + + /// @notice Compute per-asset virtual offsets v to match a vector of relative log-prices (Q64.64). + /// @dev logPrices[i] should represent ln(p_i / p_ref) for a chosen reference asset ref (e.g., ref = 0 => logPrices[0] = 0). + /// We set v_0 = 0 and for i>0 require (v_i - v_0) = b*(logPrices[i] - logPrices[0]) - (r_i - r_0). + /// Note: adding a constant to all v does not change prices; callers may shift v uniformly if they want S>0 margin. + function computeOffsetsForLogPrices( + int128 b, + int128[] memory reservesInternal, + uint256 referenceIndex, + int128[] memory logPrices + ) internal pure returns (int128[] memory vOffsets) { + require(b > int128(0), "LMSR: b<=0"); + uint256 n = reservesInternal.length; + require(logPrices.length == n, "LMSR: length mismatch"); + require(referenceIndex < n, "LMSR: ref idx"); + + vOffsets = new int128[](n); + + // Set v_ref = 0, solve differences for others + uint256 ref = referenceIndex; + int128 logP_ref = logPrices[ref]; + for (uint256 i = 0; i < n; ) { + if (i != ref) { + int128 desiredDiff = b.mul(logPrices[i].sub(logP_ref)); // b*(ln p_i - ln p_ref) + int128 currentDiff = reservesInternal[i].sub(reservesInternal[ref]); // r_i - r_ref + vOffsets[i] = desiredDiff.sub(currentDiff); // v_i - v_ref with v_ref = 0 + } + unchecked { i++; } + } + return vOffsets; } /* -------------------- @@ -53,14 +116,13 @@ library LMSRStabilized { /// @notice Cost C(q) = b * (M + ln(Z)) function cost(State storage s) internal view returns (int128) { - return cost(s.kappa, s.qInternal); + int128 b = _computeB(s); + return cost(b, s.qInternal); } /// @notice Pure version: Cost C(q) = b * (M + ln(Z)) - function cost(int128 kappa, int128[] memory qInternal) internal pure returns (int128) { - int128 sizeMetric = _computeSizeMetric(qInternal); - require(sizeMetric > int128(0), "LMSR: size metric zero"); - int128 b = kappa.mul(sizeMetric); + function cost(int128 b, int128[] memory qInternal) internal pure returns (int128) { + require(b > int128(0), "LMSR: b<=0"); (int128 M, int128 Z) = _computeMAndZ(b, qInternal); int128 lnZ = _ln(Z); int128 inner = M.add(lnZ); @@ -98,7 +160,8 @@ library LMSRStabilized { int128 a, int128 limitPrice ) internal view returns (int128 amountIn, int128 amountOut) { - return swapAmountsForExactInput(s.nAssets, s.kappa, s.qInternal, i, j, a, limitPrice); + int128 b = _computeB(s); + return swapAmountsForExactInput(s.nAssets, b, s.qInternal, i, j, a, limitPrice); } /// @notice Pure version: Closed-form asset-i -> asset-j amountOut in 64.64 fixed-point format (fee-free kernel) @@ -114,7 +177,7 @@ library LMSRStabilized { /// NOTE: Kernel is fee-free; fees should be handled by the wrapper/token layer. /// /// @param nAssets Number of assets in the pool - /// @param kappa Liquidity parameter κ (64.64 fixed point) + /// @param b Liquidity parameter κ (64.64 fixed point) /// @param qInternal Cached internal balances in 64.64 fixed-point format /// @param i Index of input asset /// @param j Index of output asset @@ -124,7 +187,7 @@ library LMSRStabilized { /// @return amountOut Amount of output asset j in 64.64 fixed-point format function swapAmountsForExactInput( uint256 nAssets, - int128 kappa, + int128 b, int128[] memory qInternal, uint256 i, uint256 j, @@ -136,58 +199,33 @@ library LMSRStabilized { // Initialize amountIn to full amount (will be adjusted if limit price is hit) amountIn = a; - // Compute b and ensure positivity before deriving invB - int128 sizeMetric = _computeSizeMetric(qInternal); - require(sizeMetric > int128(0), "LMSR: size metric zero"); - int128 b = kappa.mul(sizeMetric); + // Ensure b > 0 and derive invB require(b > int128(0), "LMSR: b<=0"); - - // Precompute reciprocal of b to avoid repeated divisions int128 invB = ABDKMath64x64.div(ONE, b); - // Guard: output asset must have non-zero effective weight to avoid degenerate/div-by-zero-like conditions - require(qInternal[j] > int128(0), "LMSR: e_j==0"); - - // Compute r0 = exp((q_i - q_j) / b) directly using invB + // Compute r0 = exp((q_i - q_j) / b) directly using invB (always > 0) int128 r0 = _exp(qInternal[i].sub(qInternal[j]).mul(invB)); - require(r0 > int128(0), "LMSR: r0<=0"); // equivalent to e_j > 0 check // If a positive limitPrice is given, determine whether the full `a` would // push the marginal price p_i/p_j beyond the limit; if so, truncate `a`. - // Marginal price ratio evolves as r(t) = r0 * exp(t/b) (since e_i multiplies by exp(t/b)) + // Marginal price ratio evolves as r(t) = r0 * exp(t/b) if (limitPrice > int128(0)) { - // r0 must be positive; if r0 == 0 then no risk of exceeding limit by increasing r. - require(r0 >= int128(0), "LMSR: r0<0"); - if (r0 == int128(0)) { -// console2.log("r0 == 0 (input asset has zero weight), no limit truncation needed"); - } else { - // If limitPrice <= current price, we revert (caller must choose a limit > current price to allow any fill) - if (limitPrice <= r0) { - revert("LMSR: limitPrice <= current price"); - } + if (limitPrice <= r0) { + revert("LMSR: limitPrice <= current price"); + } + int128 ratioLimitOverR0 = limitPrice.div(r0); + require(ratioLimitOverR0 > int128(0), "LMSR: ratio<=0"); - // Compute a_limit directly from ln(limit / r0): a_limit = b * ln(limit / r0) - int128 ratioLimitOverR0 = limitPrice.div(r0); - require(ratioLimitOverR0 > int128(0), "LMSR: ratio<=0"); - - int128 aLimitOverB = _ln(ratioLimitOverR0); // > 0 - - // aLimit = b * aLimitOverB - int128 aLimit64 = b.mul(aLimitOverB); - - // If computed aLimit is less than the requested a, use the truncated value. - if (aLimit64 < a) { - amountIn = aLimit64; // Store the truncated input amount - a = aLimit64; // Use truncated amount for calculations - } else { -// console2.log("Not truncating: aLimit64 >= a"); - } + int128 aLimitOverB = _ln(ratioLimitOverR0); // > 0 + int128 aLimit64 = b.mul(aLimitOverB); + if (aLimit64 < a) { + amountIn = aLimit64; + a = aLimit64; } } // compute a/b safely and guard against very large arguments to exp() int128 aOverB = a.mul(invB); - // Protect exp from enormous inputs (consistent with recenter thresholds) require(aOverB <= EXP_LIMIT, "LMSR: a/b too large (would overflow exp)"); // Use the closed-form fee-free formula: @@ -196,17 +234,14 @@ library LMSRStabilized { int128 oneMinusExpNeg = ONE.sub(expNeg); int128 inner = ONE.add(r0.mul(oneMinusExpNeg)); - // If inner <= 0 then cap output to the current balance q_j (cannot withdraw more than q_j) + // If inner <= 0 then return zero (numeric guard; reserve caps are enforced by wrapper) if (inner <= int128(0)) { - int128 qj64 = qInternal[j]; - return (amountIn, qj64); + return (amountIn, int128(0)); } int128 lnInner = _ln(inner); - int128 b_lnInner = b.mul(lnInner); - amountOut = b_lnInner; + amountOut = b.mul(lnInner); - // Safety check if (amountOut <= 0) { return (0, 0); } @@ -230,7 +265,8 @@ library LMSRStabilized { uint256 j, int128 limitPrice ) internal view returns (int128 amountIn, int128 amountOut) { - return swapAmountsForPriceLimit(s.nAssets, s.kappa, s.qInternal, i, j, limitPrice); + int128 b = _computeB(s); + return swapAmountsForPriceLimit(s.nAssets, b, s.qInternal, i, j, limitPrice); } /// @notice Pure version: Maximum input/output pair possible when swapping from asset i to asset j @@ -240,7 +276,7 @@ library LMSRStabilized { /// j-balance, amountOut is capped and amountIn is solved for the capped output. /// /// @param nAssets Number of assets in the pool - /// @param kappa Liquidity parameter κ (64.64 fixed point) + /// @param b Liquidity parameter κ (64.64 fixed point) /// @param qInternal Cached internal balances in 64.64 fixed-point format /// @param i Index of input asset /// @param j Index of output asset @@ -249,7 +285,7 @@ library LMSRStabilized { /// @return amountOut Corresponding maximum output amount in 64.64 fixed-point format function swapAmountsForPriceLimit( uint256 nAssets, - int128 kappa, + int128 b, int128[] memory qInternal, uint256 i, uint256 j, @@ -258,74 +294,31 @@ library LMSRStabilized { require(i < nAssets && j < nAssets, "LMSR: idx"); require(limitPrice > int128(0), "LMSR: limitPrice <= 0"); - // Compute b and ensure positivity before deriving invB - int128 sizeMetric = _computeSizeMetric(qInternal); - require(sizeMetric > int128(0), "LMSR: size metric zero"); - int128 b = kappa.mul(sizeMetric); + // Ensure positivity and derive invB require(b > int128(0), "LMSR: b<=0"); - - // Precompute reciprocal of b to avoid repeated divisions int128 invB = ABDKMath64x64.div(ONE, b); - // Guard: output asset must have non-zero effective weight to avoid degenerate/div-by-zero-like conditions - require(qInternal[j] > int128(0), "LMSR: e_j==0"); - - // Compute r0 = exp((q_i - q_j) / b) directly using invB + // Compute r0 = exp((q_i - q_j) / b) int128 r0 = _exp(qInternal[i].sub(qInternal[j]).mul(invB)); - // Mirror swapAmountsForExactInput behavior: treat invalid r0 as an error condition. - // Revert if r0 is non-positive (no finite trade under a price limit). - require(r0 > int128(0), "LMSR: r0<=0"); - - // If current price already exceeds or equals limit, revert the same way swapAmountsForExactInput does. + // If current price already exceeds or equals limit, revert (no room to move) if (r0 >= limitPrice) { revert("LMSR: limitPrice <= current price"); } - // Calculate the price change factor: limitPrice/r0 - int128 priceChangeFactor = limitPrice.div(r0); - - // ln(priceChangeFactor) gives us the maximum allowed delta in the exponent - int128 maxDeltaExponent = _ln(priceChangeFactor); - - // Maximum input capable of reaching the price limit: // x_max = b * ln(limitPrice / r0) + int128 priceChangeFactor = limitPrice.div(r0); + int128 maxDeltaExponent = _ln(priceChangeFactor); int128 amountInMax = b.mul(maxDeltaExponent); - // The maximum output y corresponding to that input: - // y = b * ln(1 + (e_i/e_j) * (1 - exp(-x_max/b))) + // y_max = b * ln(1 + r0 * (1 - exp(-x_max/b))) int128 expTerm = ONE.sub(_exp(maxDeltaExponent.neg())); int128 innerTerm = r0.mul(expTerm); int128 lnTerm = _ln(ONE.add(innerTerm)); int128 maxOutput = b.mul(lnTerm); - // Current balance of asset j (in 64.64) - int128 qj64 = qInternal[j]; - - // Initialize outputs to the computed maxima amountIn = amountInMax; amountOut = maxOutput; - - // If the calculated maximum output exceeds the balance, cap output and solve for input. - if (maxOutput > qj64) { - amountOut = qj64; - - // Solve inverse relation for input given capped output: - // Given y = amountOut, let E = exp(y/b). Then - // 1 - exp(-a/b) = (E - 1) / r0 - // exp(-a/b) = 1 - (E - 1) / r0 = (r0 + 1 - E) / r0 - // a = -b * ln( (r0 + 1 - E) / r0 ) = b * ln( r0 / (r0 + 1 - E) ) - int128 E = _exp(amountOut.mul(invB)); // exp(y/b) - int128 rhs = r0.add(ONE).sub(E); // r0 + 1 - E - - // If rhs <= 0 due to numerical issues, fall back to amountInMax - if (rhs <= int128(0)) { - amountIn = amountInMax; - } else { - amountIn = b.mul(_ln(r0.div(rhs))); - } - } - return (amountIn, amountOut); } @@ -340,7 +333,8 @@ library LMSRStabilized { uint256 i, int128 a ) internal view returns (int128 amountIn, int128 amountOut) { - return swapAmountsForMint(s.nAssets, s.kappa, s.qInternal, i, a); + int128 b = _computeB(s); + return swapAmountsForMint(s.nAssets, b, s.qInternal, i, a); } /// @notice Pure version: Compute LP-size increase when minting from a single-token input using bisection only. @@ -350,7 +344,7 @@ library LMSRStabilized { /// x_j = b * ln( r0_j / (r0_j + 1 - exp(y_j / b)) ), r0_j = exp((q_i - q_j)/b). /// Bisection is used (no Newton) to keep implementation compact and gas-friendly. /// @param nAssets Number of assets in the pool - /// @param kappa Liquidity parameter κ (64.64 fixed point) + /// @param b Liquidity parameter κ (64.64 fixed point) /// @param qInternal Cached internal balances in 64.64 fixed-point format /// @param i Index of input asset /// @param a Amount of input asset (in int128 format, 64.64 fixed-point) @@ -358,7 +352,7 @@ library LMSRStabilized { /// @return amountOut LP size-metric increase (alpha * S) function swapAmountsForMint( uint256 nAssets, - int128 kappa, + int128 b, int128[] memory qInternal, uint256 i, int128 a @@ -366,12 +360,11 @@ library LMSRStabilized { require(i < nAssets, "LMSR: idx"); require(a > int128(0), "LMSR: amount <= 0"); - int128 sizeMetric = _computeSizeMetric(qInternal); - require(sizeMetric > int128(0), "LMSR: size metric zero"); - int128 b = kappa.mul(sizeMetric); + // Compute size metric S for output computation; use provided b for curvature + int128 S = _computeSizeMetric(qInternal); + require(S > int128(0), "LMSR: size metric zero"); require(b > int128(0), "LMSR: b<=0"); int128 invB = ABDKMath64x64.div(ONE, b); - int128 S = sizeMetric; uint256 n = nAssets; @@ -552,7 +545,8 @@ library LMSRStabilized { uint256 i, int128 alpha ) internal view returns (int128 amountOut, int128 amountIn) { - return swapAmountsForBurn(s.nAssets, s.kappa, s.qInternal, i, alpha); + int128 b = _computeB(s); + return swapAmountsForBurn(s.nAssets, b, s.qInternal, i, alpha); } /// @notice Pure version: Compute single-asset payout when burning a proportional share alpha of the pool. @@ -565,7 +559,7 @@ library LMSRStabilized { /// Treat any per-asset rhs<=0 as "this asset contributes zero" (do not revert). /// Revert only if the final single-asset payout is zero. /// @param nAssets Number of assets in the pool - /// @param kappa Liquidity parameter κ (64.64 fixed point) + /// @param b Liquidity parameter κ (64.64 fixed point) /// @param qInternal Cached internal balances in 64.64 fixed-point format /// @param i Index of output asset /// @param alpha Proportional share to burn (0 < alpha <= 1) @@ -573,7 +567,7 @@ library LMSRStabilized { /// @return amountIn LP size-metric redeemed (alpha * S) function swapAmountsForBurn( uint256 nAssets, - int128 kappa, + int128 b, int128[] memory qInternal, uint256 i, int128 alpha @@ -581,9 +575,9 @@ library LMSRStabilized { require(i < nAssets, "LMSR: idx"); require(alpha > int128(0) && alpha <= ONE, "LMSR: alpha"); + // Compute size metric S for payout normalization; use provided b for curvature int128 sizeMetric = _computeSizeMetric(qInternal); require(sizeMetric > int128(0), "LMSR: size metric zero"); - int128 b = kappa.mul(sizeMetric); require(b > int128(0), "LMSR: b<=0"); int128 invB = ABDKMath64x64.div(ONE, b); @@ -726,20 +720,47 @@ library LMSRStabilized { } } + /// @notice Optional helper: recompute qInternal from reserves and explicit virtual offsets. + /// @dev This variant is useful if callers manage virtual offsets off-chain and want to preserve price neutrality + /// across proportional mints/burns by applying updated offsets alongside reserve changes. + /// @param newReservesInternal New reserves vector in 64.64 (converted from uint balances) + /// @param vOffsets Per-asset virtual offsets to apply (same length as nAssets) + function updateForProportionalChangeWithOffsets( + State storage s, + int128[] memory newReservesInternal, + int128[] memory vOffsets + ) internal { + require(newReservesInternal.length == s.nAssets, "LMSR: length mismatch"); + require(vOffsets.length == s.nAssets, "LMSR: v length mismatch"); + + int128[] memory combined = new int128[](s.nAssets); + for (uint i = 0; i < s.nAssets; ) { + combined[i] = newReservesInternal[i].add(vOffsets[i]); + unchecked { i++; } + } + + int128 newTotal = _computeSizeMetric(combined); + require(newTotal > int128(0), "LMSR: new total zero"); + + for (uint i = 0; i < s.nAssets; ) { + s.qInternal[i] = combined[i]; + unchecked { i++; } + } + } + /// @notice Price-share of asset i: exp(z_i) / Z (64.64) function priceShare(State storage s, uint256 i) internal view returns (int128) { - return priceShare(s.kappa, s.qInternal, i); + int128 b = _computeB(s); + return priceShare(b, s.qInternal, i); } /// @notice Pure version: Price-share of asset i: exp(z_i) / Z (64.64) - /// @param kappa Liquidity parameter κ (64.64 fixed point) + /// @param b Liquidity parameter κ (64.64 fixed point) /// @param qInternal Cached internal balances in 64.64 fixed-point format /// @param i Index of asset /// @return Price share in 64.64 fixed-point format - function priceShare(int128 kappa, int128[] memory qInternal, uint256 i) internal pure returns (int128) { - int128 sizeMetric = _computeSizeMetric(qInternal); - require(sizeMetric > int128(0), "LMSR: size metric zero"); - int128 b = kappa.mul(sizeMetric); + function priceShare(int128 b, int128[] memory qInternal, uint256 i) internal pure returns (int128) { + require(b > int128(0), "LMSR: b<=0"); uint len = qInternal.length; require(len > 0, "LMSR: no assets"); @@ -797,22 +818,20 @@ library LMSRStabilized { /// @notice Marginal price of `base` in terms of `quote` (p_quote / p_base) as Q64.64 /// @dev Returns exp((q_quote - q_base) / b). Indices must be valid and b > 0. function price(State storage s, uint256 baseTokenIndex, uint256 quoteTokenIndex) internal view returns (int128) { - return price(s.nAssets, s.kappa, s.qInternal, baseTokenIndex, quoteTokenIndex); + int128 b = _computeB(s); + return price(s.nAssets, b, s.qInternal, baseTokenIndex, quoteTokenIndex); } /// @notice Pure version: Marginal price of `base` in terms of `quote` (p_quote / p_base) as Q64.64 /// @dev Returns exp((q_quote - q_base) / b). Indices must be valid and b > 0. /// @param nAssets Number of assets in the pool - /// @param kappa Liquidity parameter κ (64.64 fixed point) + /// @param b Liquidity parameter κ (64.64 fixed point) /// @param qInternal Cached internal balances in 64.64 fixed-point format /// @param baseTokenIndex Index of base token /// @param quoteTokenIndex Index of quote token /// @return Price in 64.64 fixed-point format - function price(uint256 nAssets, int128 kappa, int128[] memory qInternal, uint256 baseTokenIndex, uint256 quoteTokenIndex) internal pure returns (int128) { + function price(uint256 nAssets, int128 b, int128[] memory qInternal, uint256 baseTokenIndex, uint256 quoteTokenIndex) internal pure returns (int128) { require(baseTokenIndex < nAssets && quoteTokenIndex < nAssets, "LMSR: idx"); - int128 sizeMetric = _computeSizeMetric(qInternal); - require(sizeMetric > int128(0), "LMSR: size metric zero"); - int128 b = kappa.mul(sizeMetric); require(b > int128(0), "LMSR: b<=0"); // Use reciprocal of b to avoid repeated divisions @@ -825,26 +844,22 @@ library LMSRStabilized { /// @notice Price of one unit of the LP size-metric (S = sum q_i) denominated in `quote` asset (Q64.64) /// @dev Computes: poolPrice_quote = (1 / S) * sum_j q_j * exp((q_j - q_quote) / b) function poolPrice(State storage s, uint256 quoteTokenIndex) internal view returns (int128) { - return poolPrice(s.nAssets, s.kappa, s.qInternal, quoteTokenIndex); + int128 b = _computeB(s); + return poolPrice(s.nAssets, b, s.qInternal, quoteTokenIndex); } /// @notice Pure version: Price of one unit of the LP size-metric (S = sum q_i) denominated in `quote` asset (Q64.64) /// @dev Computes: poolPrice_quote = (1 / S) * sum_j q_j * exp((q_j - q_quote) / b) /// @param nAssets Number of assets in the pool - /// @param kappa Liquidity parameter κ (64.64 fixed point) + /// @param b Liquidity parameter κ (64.64 fixed point) /// @param qInternal Cached internal balances in 64.64 fixed-point format /// @param quoteTokenIndex Index of quote token /// @return Pool price in 64.64 fixed-point format - function poolPrice(uint256 nAssets, int128 kappa, int128[] memory qInternal, uint256 quoteTokenIndex) internal pure returns (int128) { + function poolPrice(uint256 nAssets, int128 b, int128[] memory qInternal, uint256 quoteTokenIndex) internal pure returns (int128) { require(quoteTokenIndex < nAssets, "LMSR: idx"); - // Compute b and ensure positivity - int128 sizeMetric = _computeSizeMetric(qInternal); - require(sizeMetric > int128(0), "LMSR: size metric zero"); - int128 b = kappa.mul(sizeMetric); + // Ensure positivity and compute total size metric S = sum q_i require(b > int128(0), "LMSR: b<=0"); - - // Compute total size metric S = sum q_i - int128 S = sizeMetric; + int128 S = _computeSizeMetric(qInternal); require(S > int128(0), "LMSR: size zero"); // Precompute reciprocal of b @@ -870,9 +885,8 @@ library LMSRStabilized { Slippage -> b computation & resize-triggered rescale -------------------- */ - /// @notice Internal helper to compute kappa from slippage parameters. - /// @dev Returns κ in Q64.64. Implemented as internal so callers within the library can use it - /// without resorting to external calls. + /// @notice Internal helper to compute kappa from slippage parameters. (Deprecated in favor of computeBFromSlippage) + /// @dev Returns κ in Q64.64 for reference. Prefer computing b directly via computeBFromSlippage. function computeKappaFromSlippage( uint256 nAssets, int128 tradeFrac, @@ -920,7 +934,57 @@ library LMSRStabilized { return kappa; } - /// @notice Legacy-compatible init: compute kappa from slippage parameters and delegate to kappa-based init. + /// @notice Compute fixed b from a target slippage profile. + /// @dev For a trade of fraction f of the size metric S, targeting slippage s, we have y := -ln(E)/f with + /// E = (1 - s*(n-1)) / (1 + s) and b = S / y. + function computeBFromSlippage( + uint256 nAssets, + int128 sizeMetricS, + int128 tradeFrac, + int128 targetSlippage + ) internal pure returns (int128) { + require(nAssets > 1, "LMSR: n>1 required"); + require(sizeMetricS > int128(0), "LMSR: S<=0"); + + // f must be in (0,1) + int128 f = tradeFrac; + require(f > int128(0), "LMSR: f=0"); + require(f < ONE, "LMSR: f>=1"); + + int128 onePlusS = ONE.add(targetSlippage); + + int128 n64 = ABDKMath64x64.fromUInt(nAssets); + int128 nMinus1_64 = ABDKMath64x64.fromUInt(nAssets - 1); + + // If 1 + s >= n then equal-inventories closed-form applies + bool useEqual = (onePlusS >= n64); + + // E candidate used in deriving y = -ln(E)/f (same expression in both branches) + int128 numerator = ONE.sub(targetSlippage.mul(nMinus1_64)); // 1 - s*(n-1) + int128 denominator = onePlusS; // 1 + s + + if (useEqual) { + // Guard numerator to ensure E in (0,1) + require(numerator > int128(0), "LMSR: s too large for n"); + } else { + require(numerator > int128(0), "LMSR: bad slippage or n"); + } + + int128 E_candidate = numerator.div(denominator); + require(E_candidate > int128(0) && E_candidate < ONE, "LMSR: bad E ratio"); + + // y = -ln(E) / f + int128 lnE = _ln(E_candidate); + int128 y = lnE.neg().div(f); + require(y > int128(0), "LMSR: y<=0"); + + // b = S / y + int128 b = ABDKMath64x64.div(sizeMetricS, y); + require(b > int128(0), "LMSR: b<=0"); + return b; + } + + /// @notice Legacy-compatible init: compute fixed b from slippage parameters and delegate to fixed-b init. /// @dev Provides backward compatibility for callers that still use the (q, tradeFrac, targetSlippage) init signature. function init( State storage s, @@ -928,10 +992,11 @@ library LMSRStabilized { int128 tradeFrac, int128 targetSlippage ) internal { - // compute kappa using the internal helper - int128 kappa = computeKappaFromSlippage(initialQInternal.length, tradeFrac, targetSlippage); - // forward to the new kappa-based init - init(s, initialQInternal, kappa); + // compute b directly from the current size metric and the slippage profile + int128 S = _computeSizeMetric(initialQInternal); + int128 b = computeBFromSlippage(initialQInternal.length, S, tradeFrac, targetSlippage); + // forward to the fixed-b init + init(s, initialQInternal, b); } @@ -940,12 +1005,12 @@ library LMSRStabilized { function deinit(State storage s) internal { // Reset core state s.nAssets = 0; - s.kappa = int128(0); + s.bFixed = int128(0); // Clear qInternal array delete s.qInternal; - // Note: init(...) will recompute kappa and nAssets on first mint. + // Note: initWithFixedB(...) will set b and nAssets on first mint. } /// @notice Compute M (shift) and Z (sum of exponentials) dynamically @@ -1023,9 +1088,8 @@ library LMSRStabilized { /// @notice Compute b from kappa and current asset quantities function _computeB(State storage s) internal view returns (int128) { - int128 sizeMetric = _computeSizeMetric(s.qInternal); - require(sizeMetric > int128(0), "LMSR: size metric zero"); - return s.kappa.mul(sizeMetric); + // require(s.bFixed > int128(0), "LMSR: b not set"); + return s.bFixed; } } diff --git a/src/PartyPool.sol b/src/PartyPool.sol index 0fdd2fa..47442df 100644 --- a/src/PartyPool.sol +++ b/src/PartyPool.sol @@ -48,11 +48,9 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool function wrapperToken() external view returns (NativeWrapper) { return WRAPPER_TOKEN; } - /// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q) - /// @dev Pool is constructed with a fixed κ. Clients that previously passed tradeFrac/targetSlippage - /// should use LMSRStabilized.computeKappaFromSlippage(...) to derive κ and pass it here. - int128 private immutable KAPPA; // kappa in Q64.64 - function kappa() external view returns (int128) { return KAPPA; } + /// @notice Fixed LMSR curvature parameter b (Q64.64) + int128 private immutable B_FIXED; + function bFixed() external view returns (int128) { return B_FIXED; } /// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations. uint256 private immutable SWAP_FEE_PPM; @@ -100,7 +98,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool /// @param symbol_ LP token symbol /// @param tokens_ token addresses (n) /// @param bases_ scaling _bases for each token (n) - used when converting to/from internal 64.64 amounts - /// @param kappa_ liquidity parameter κ (Q64.64) used to derive b = κ * S(q) + /// @param bFixed_ fixed LMSR curvature b (Q64.64) /// @param swapFeePpm_ fee in parts-per-million, taken from swap input amounts before LMSR calculations /// @param flashFeePpm_ fee in parts-per-million, taken for flash loans /// @param swapImpl_ address of the SwapMint implementation contract @@ -111,7 +109,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool string memory symbol_, IERC20[] memory tokens_, uint256[] memory bases_, - int128 kappa_, + int128 bFixed_, uint256 swapFeePpm_, uint256 flashFeePpm_, uint256 protocolFeePpm_, @@ -129,7 +127,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool require(tokens_.length == bases_.length, "Pool: lengths mismatch"); _tokens = tokens_; _bases = bases_; - KAPPA = kappa_; + B_FIXED = bFixed_; require(swapFeePpm_ < 1_000_000, "Pool: fee >= ppm"); SWAP_FEE_PPM = swapFeePpm_; require(flashFeePpm_ < 1_000_000, "Pool: flash fee >= ppm"); @@ -184,7 +182,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool PartyPoolMintImpl.initialMint.selector, receiver, lpTokens, - KAPPA + B_FIXED ); bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data); return abi.decode(result, (uint256)); diff --git a/src/PartyPoolMintImpl.sol b/src/PartyPoolMintImpl.sol index 121432e..63d3385 100644 --- a/src/PartyPoolMintImpl.sol +++ b/src/PartyPoolMintImpl.sol @@ -25,7 +25,7 @@ contract PartyPoolMintImpl is PartyPoolBase { // Initialization Mint // - function initialMint(address receiver, uint256 lpTokens, int128 KAPPA) external payable native killable nonReentrant + function initialMint(address receiver, uint256 lpTokens, int128 B_FIXED) external payable native killable nonReentrant returns (uint256 lpMinted) { uint256 n = _tokens.length; @@ -44,8 +44,8 @@ contract PartyPoolMintImpl is PartyPoolBase { unchecked { i++; } } - // Initialize the stabilized LMSR state with provided kappa - _lmsr.init(newQInternal, KAPPA); + // Initialize the stabilized LMSR state with provided fixed b + _lmsr.init(newQInternal, B_FIXED); // Compute actual LP _tokens to mint based on size metric (scaled) if( lpTokens != 0 ) @@ -288,7 +288,7 @@ contract PartyPoolMintImpl is PartyPoolBase { // Use LMSR view to determine actual internal consumed and size-increase (ΔS) for mint (int128 amountInInternalUsed, int128 sizeIncreaseInternal) = - LMSRStabilized.swapAmountsForMint(lmsrState.nAssets, lmsrState.kappa, lmsrState.qInternal, + LMSRStabilized.swapAmountsForMint(lmsrState.nAssets, lmsrState.bFixed, lmsrState.qInternal, inputTokenIndex, netInternalGuess); // amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer @@ -456,7 +456,7 @@ contract PartyPoolMintImpl is PartyPoolBase { .mul(ABDKMath64x64.divu(1000000-swapFeePpm, 1000000)); // adjusted for fee // Use LMSR view to compute single-asset payout and burned size-metric - (int128 payoutInternal, ) = LMSRStabilized.swapAmountsForBurn(lmsrState.nAssets, lmsrState.kappa, lmsrState.qInternal, + (int128 payoutInternal, ) = LMSRStabilized.swapAmountsForBurn(lmsrState.nAssets, lmsrState.bFixed, lmsrState.qInternal, inputTokenIndex, alpha); // Convert payoutInternal -> uint (floor) to favor pool diff --git a/src/PartyPoolSwapImpl.sol b/src/PartyPoolSwapImpl.sol index cddbeb6..32afd3a 100644 --- a/src/PartyPoolSwapImpl.sol +++ b/src/PartyPoolSwapImpl.sol @@ -24,13 +24,13 @@ contract PartyPoolSwapImpl is PartyPoolBase { uint256 outputTokenIndex, int128 limitPrice, uint256[] memory bases, - int128 kappa, + int128 b, int128[] memory qInternal, uint256 swapFeePpm ) external pure returns (uint256 amountIn, uint256 amountOut, uint256 fee) { // Compute internal maxima at the price limit (int128 amountInInternal, int128 amountOutInternal) = LMSRStabilized.swapAmountsForPriceLimit( - bases.length, kappa, qInternal, + bases.length, b, qInternal, inputTokenIndex, outputTokenIndex, limitPrice); // Convert input to uint (ceil) and output to uint (floor) diff --git a/src/PartyPoolViewer.sol b/src/PartyPoolViewer.sol index 4cf2027..1063de8 100644 --- a/src/PartyPoolViewer.sol +++ b/src/PartyPoolViewer.sol @@ -35,7 +35,7 @@ contract PartyPoolViewer is PartyPoolHelpers, IPartyPoolViewer { LMSRStabilized.State memory lmsr = pool.LMSR(); require(baseTokenIndex < lmsr.nAssets && quoteTokenIndex < lmsr.nAssets, "price: idx"); require(lmsr.nAssets > 0, "price: uninit"); - return LMSRStabilized.price(lmsr.nAssets, pool.kappa(), lmsr.qInternal, baseTokenIndex, quoteTokenIndex); + return LMSRStabilized.price(lmsr.nAssets, pool.bFixed(), lmsr.qInternal, baseTokenIndex, quoteTokenIndex); } /// @notice Price of one LP token denominated in `quote` as Q64.64. @@ -50,7 +50,7 @@ contract PartyPoolViewer is PartyPoolHelpers, IPartyPoolViewer { require(quoteTokenIndex < lmsr.nAssets, "poolPrice: idx"); // price per unit of qTotal (Q64.64) from LMSR - int128 pricePerQ = LMSRStabilized.poolPrice(lmsr.nAssets, pool.kappa(), lmsr.qInternal, quoteTokenIndex); + int128 pricePerQ = LMSRStabilized.poolPrice(lmsr.nAssets, pool.bFixed(), lmsr.qInternal, quoteTokenIndex); // total internal q (qTotal) as Q64.64 int128 qTotal = LMSRStabilized._computeSizeMetric(lmsr.qInternal); @@ -105,7 +105,7 @@ contract PartyPoolViewer is PartyPoolHelpers, IPartyPoolViewer { return SWAP_IMPL.swapToLimitAmounts( inputTokenIndex, outputTokenIndex, limitPrice, - pool.denominators(), pool.kappa(), lmsr.qInternal, pool.swapFeePpm()); + pool.denominators(), pool.bFixed(), lmsr.qInternal, pool.swapFeePpm()); } diff --git a/test/GasTest.sol b/test/GasTest.sol index a21bfef..e5b999e 100644 --- a/test/GasTest.sol +++ b/test/GasTest.sol @@ -114,6 +114,22 @@ contract GasTest is Test { uint256 constant internal INIT_BAL = 1_000_000; // initial token units for each token (internal==amount when base==1) uint256 constant internal BASE = 1; // use base=1 so internal amounts correspond to raw integers (Q64.64 units) + // Compute fixed b from a target slippage profile for a pool that will be initialized + // with numTokens tokens each deposited with INIT_BAL and BASE==1. + function _computeFixedB(uint256 numTokens) internal view returns (int128) { + // Size metric S = sum q_i (in 64.64) with q_i = INIT_BAL for each token (BASE == 1) + int128 S = ABDKMath64x64.fromUInt(numTokens * INIT_BAL); + // E = (1 - s*(n-1)) / (1 + s) + int128 one = ABDKMath64x64.fromInt(1); + int128 nMinus1 = ABDKMath64x64.fromUInt(numTokens - 1); + int128 numerator = one.sub(targetSlippage.mul(nMinus1)); + int128 denominator = one.add(targetSlippage); + int128 E = numerator.div(denominator); + // y = -ln(E) / f, b = S / y + int128 y = ABDKMath64x64.ln(E).neg().div(tradeFrac); + return S.div(y); + } + /// @notice Helper function to create a pool with the specified number of _tokens function createPool(uint256 numTokens) internal returns (PartyPool) { // Deploy _tokens dynamically @@ -139,9 +155,9 @@ contract GasTest is Test { for (uint i = 0; i < tokens.length; i++) { ierc20Tokens[i] = IERC20(tokens[i]); } - // Compute kappa from slippage params and number of _tokens, then construct pool with kappa - int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(ierc20Tokens.length, tradeFrac, targetSlippage); - PartyPool newPool = Deploy.newPartyPool(address(this), poolName, poolName, ierc20Tokens, bases, computedKappa, feePpm, feePpm, false); + // Compute fixed b from slippage and expected initial S, then construct pool with fixed b + int128 bFixed = _computeFixedB(ierc20Tokens.length); + PartyPool newPool = Deploy.newPartyPool(address(this), poolName, poolName, ierc20Tokens, bases, bFixed, feePpm, feePpm, false); // Transfer initial deposit amounts into pool before initial mint for (uint256 i = 0; i < numTokens; i++) { @@ -180,8 +196,8 @@ contract GasTest is Test { for (uint i = 0; i < tokens.length; i++) { ierc20Tokens[i] = IERC20(tokens[i]); } - int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(ierc20Tokens.length, tradeFrac, targetSlippage); - PartyPool newPool = Deploy.newPartyPool(address(this), poolName, poolName, ierc20Tokens, bases, computedKappa, feePpm, feePpm, true); + int128 bFixed = _computeFixedB(ierc20Tokens.length); + PartyPool newPool = Deploy.newPartyPool(address(this), poolName, poolName, ierc20Tokens, bases, bFixed, feePpm, feePpm, true); // Transfer initial deposit amounts into pool before initial mint for (uint256 i = 0; i < numTokens; i++) { diff --git a/test/LMSRStabilized.t.sol b/test/LMSRStabilized.t.sol index 5aec7e1..5269f91 100644 --- a/test/LMSRStabilized.t.sol +++ b/test/LMSRStabilized.t.sol @@ -30,7 +30,8 @@ contract LMSRStabilizedTest is Test { q[0] = ABDKMath64x64.fromUInt(1_000_000); q[1] = ABDKMath64x64.fromUInt(1_000_000); q[2] = ABDKMath64x64.fromUInt(1_000_000); - s.init(q, stdTradeSize, stdSlippage); + int128 b = _computeBFromSlippage(3, q, stdTradeSize, stdSlippage); + s.init(q, b); } function initAlmostBalanced() internal { @@ -38,7 +39,8 @@ contract LMSRStabilizedTest is Test { q[0] = ABDKMath64x64.fromUInt(999_999); q[1] = ABDKMath64x64.fromUInt(1_000_000); q[2] = ABDKMath64x64.fromUInt(1_000_001); - s.init(q, stdTradeSize, stdSlippage); + int128 b = _computeBFromSlippage(3, q, stdTradeSize, stdSlippage); + s.init(q, b); } function initImbalanced() internal { @@ -47,7 +49,8 @@ contract LMSRStabilizedTest is Test { q[1] = ABDKMath64x64.fromUInt(1e9); q[2] = ABDKMath64x64.fromUInt(1); q[3] = ABDKMath64x64.divu(1, 1e9); - s.init(q, stdTradeSize, stdSlippage); + int128 b = _computeBFromSlippage(4, q, stdTradeSize, stdSlippage); + s.init(q, b); } @@ -193,7 +196,6 @@ contract LMSRStabilizedTest is Test { // Verify basic state is still functional assertTrue(s.nAssets > 0, "State should still be initialized"); - assertTrue(s.kappa > int128(0), "Kappa should still be positive"); } function testRescalingAfterDeposit() public { @@ -211,7 +213,6 @@ contract LMSRStabilizedTest is Test { // Store initial parameters int128 initialB = _computeB(initialQ); - int128 initialKappa = s.kappa; // Simulate a deposit by increasing all asset quantities by 50% int128[] memory newQ = new int128[](s.nAssets); @@ -223,18 +224,15 @@ contract LMSRStabilizedTest is Test { // Apply the update for proportional change s.updateForProportionalChange(newQ); - // Verify that b has been rescaled proportionally + // Verify that b remains constant after proportional deposit (fixed-b model) int128 newB = _computeB(s.qInternal); - int128 expectedRatio = ABDKMath64x64.fromUInt(3).div(ABDKMath64x64.fromUInt(2)); // 1.5x + int128 expectedRatio = ABDKMath64x64.fromInt(1); // invariant b int128 actualRatio = newB.div(initialB); int128 tolerance = ABDKMath64x64.divu(1, 1000); // 0.1% tolerance - assertTrue((actualRatio.sub(expectedRatio)).abs() < tolerance, "b did not scale proportionally after deposit"); + assertTrue((actualRatio.sub(expectedRatio)).abs() < tolerance, "b should remain constant after deposit"); - // Verify kappa remained unchanged - assertTrue((s.kappa.sub(initialKappa)).abs() < tolerance, "kappa should not change after deposit"); - - // Verify slippage target is still met by performing a trade + // Perform a trade and verify outputs are reasonable int128 tradeAmount = s.qInternal[0].mul(stdTradeSize); (int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(0, 1, tradeAmount, 0); @@ -250,8 +248,10 @@ contract LMSRStabilizedTest is Test { int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1)); console2.log('post-deposit slippage', slippage); - int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage); - assertLt(relativeError, ABDKMath64x64.divu(1, 100), "Slippage target not met after deposit"); + // With fixed b, theoretical slippage is exp(a/b) - 1 + int128 expectedSlippage = _exp(tradeAmount.div(newB)).sub(ABDKMath64x64.fromInt(1)); + int128 slippageError = (slippage.sub(expectedSlippage)).abs(); + assertLt(slippageError, ABDKMath64x64.divu(1, 1_000_000), "Observed slippage deviates from model"); } /// @notice Test balanced2 handling of limitPrice that causes truncation of input a @@ -260,7 +260,8 @@ contract LMSRStabilizedTest is Test { int128[] memory q = new int128[](2); q[0] = ABDKMath64x64.fromUInt(1_000_000); q[1] = ABDKMath64x64.fromUInt(1_000_000); - s.init(q, stdTradeSize, stdSlippage); + int128 bInit = _computeBFromSlippage(2, q, stdTradeSize, stdSlippage); + s.init(q, bInit); // Compute b for constructing meaningful a and limits int128 b = _computeB(q); @@ -296,7 +297,8 @@ contract LMSRStabilizedTest is Test { int128[] memory q = new int128[](2); q[0] = ABDKMath64x64.fromUInt(1_000_000); q[1] = ABDKMath64x64.fromUInt(1_000_000); - s.init(q, stdTradeSize, stdSlippage); + int128 bInit = _computeBFromSlippage(2, q, stdTradeSize, stdSlippage); + s.init(q, bInit); // Small input a int128 a = q[0].mul(ABDKMath64x64.divu(1, 1000)); // 0.1% of asset @@ -326,7 +328,8 @@ contract LMSRStabilizedTest is Test { int128[] memory q = new int128[](2); q[0] = ABDKMath64x64.fromUInt(1_000_000); q[1] = ABDKMath64x64.fromUInt(1_000_000); - s.init(q, stdTradeSize, stdSlippage); + int128 bInit = _computeBFromSlippage(2, q, stdTradeSize, stdSlippage); + s.init(q, bInit); int128 limitPrice = ABDKMath64x64.fromInt(1); // equal to current price @@ -359,7 +362,6 @@ contract LMSRStabilizedTest is Test { // Store initial parameters int128 initialB = _computeB(initialQ); - int128 initialKappa = s.kappa; // Simulate a withdrawal by decreasing all asset quantities by 30% int128[] memory newQ = new int128[](s.nAssets); @@ -371,18 +373,15 @@ contract LMSRStabilizedTest is Test { // Apply the update for proportional change s.updateForProportionalChange(newQ); - // Verify that b has been rescaled proportionally + // Verify that b remains constant after proportional withdrawal (fixed-b model) int128 newB = _computeB(s.qInternal); - int128 expectedRatio = ABDKMath64x64.fromUInt(7).div(ABDKMath64x64.fromUInt(10)); // 0.7x + int128 expectedRatio = ABDKMath64x64.fromInt(1); // invariant b int128 actualRatio = newB.div(initialB); int128 tolerance = ABDKMath64x64.divu(1, 1000); // 0.1% tolerance - assertTrue((actualRatio.sub(expectedRatio)).abs() < tolerance, "b did not scale proportionally after withdrawal"); + assertTrue((actualRatio.sub(expectedRatio)).abs() < tolerance, "b should remain constant after withdrawal"); - // Verify kappa remained unchanged - assertTrue((s.kappa.sub(initialKappa)).abs() < tolerance, "kappa should not change after withdrawal"); - - // Verify slippage target is still met by performing a trade + // Perform a trade and verify outputs are reasonable int128 tradeAmount = s.qInternal[0].mul(stdTradeSize); (int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(0, 1, tradeAmount, 0); @@ -398,8 +397,10 @@ contract LMSRStabilizedTest is Test { int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1)); console2.log('post-withdrawal slippage', slippage); - int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage); - assertLt(relativeError, ABDKMath64x64.divu(1, 100), "Slippage target not met after withdrawal"); + // With fixed b, theoretical slippage is exp(a/b) - 1 + int128 expectedSlippage = _exp(tradeAmount.div(newB)).sub(ABDKMath64x64.fromInt(1)); + int128 slippageError = (slippage.sub(expectedSlippage)).abs(); + assertLt(slippageError, ABDKMath64x64.divu(1, 1_000_000), "Observed slippage deviates from model"); } // --- tests probing numerical stability and boundary conditions --- @@ -431,8 +432,8 @@ contract LMSRStabilizedTest is Test { this.externalSwapAmountsForExactInput(0, 1, tradeAmount, ABDKMath64x64.fromInt(1)); } - /// @notice If e_j == 0 we should revert early to avoid div-by-zero - function testEJZeroReverts() public { + /// @notice If q_j == 0, kernel should still handle computation without revert (wrapper enforces caps) + function testZeroQuantityOutputAssetDoesNotRevert() public { initBalanced(); // Create mock qInternal where asset 1 has zero quantity @@ -446,8 +447,10 @@ contract LMSRStabilizedTest is Test { int128 tradeAmount = mockQInternal[0].mul(stdTradeSize); - vm.expectRevert(bytes("LMSR: e_j==0")); - this.externalSwapAmountsForExactInput(0, 1, tradeAmount, 0); + // Should not revert; exact-input uses full input and returns a defined output + (int128 usedIn, int128 outAmt) = this.externalSwapAmountsForExactInput(0, 1, tradeAmount, 0); + assertEq(usedIn, tradeAmount, "exact-input should consume full input without limit"); + assertTrue(outAmt >= 0, "output amount should be non-negative when q_j == 0"); } /// @notice swapAmountsForPriceLimit returns zero if limit equals current price @@ -534,18 +537,16 @@ contract LMSRStabilizedTest is Test { this.externalSwapAmountsForExactInput(0, 1, a, 0); } - // Helper function to compute b from qInternal (either from provided array or state) + // Helper function to fetch fixed b (independent of qInternal) function _computeB(int128[] memory qInternal) internal view returns (int128) { - int128 sizeMetric = _computeSizeMetric(qInternal); - require(sizeMetric > int128(0), "LMSR: size metric zero"); - return s.kappa.mul(sizeMetric); + // silence unused warning for qInternal in tests + qInternal; + return s.bFixed; } - // Overload that uses state's cached qInternal + // Overload that uses state's fixed b function _computeB() internal view returns (int128) { - int128 sizeMetric = _computeSizeMetric(s.qInternal); - require(sizeMetric > int128(0), "LMSR: size metric zero"); - return s.kappa.mul(sizeMetric); + return s.bFixed; } // Helper function to compute size metric (sum of all asset quantities) @@ -558,6 +559,41 @@ contract LMSRStabilizedTest is Test { return total; } + // Local helper: compute fixed b from a target slippage profile. + // For a trade of fraction f of S with target slippage s across n assets: + // E = (1 - s*(n-1)) / (1 + s) + // y = -ln(E) / f + // b = S / y + function _computeBFromSlippage( + uint256 nAssets, + int128[] memory qInternal, + int128 tradeFrac, + int128 targetSlippage + ) internal pure returns (int128) { + require(nAssets > 1, "test: n>1"); + int128 S = _computeSizeMetric(qInternal); + require(S > int128(0), "test: S<=0"); + + int128 f = tradeFrac; + require(f > int128(0) && f < ABDKMath64x64.fromInt(1), "test: f out of range"); + + int128 one = ABDKMath64x64.fromInt(1); + int128 nMinus1 = ABDKMath64x64.fromUInt(nAssets - 1); + + // E must be in (0,1) + int128 numerator = one.sub(targetSlippage.mul(nMinus1)); // 1 - s*(n-1) + int128 denominator = one.add(targetSlippage); // 1 + s + require(numerator > int128(0), "test: bad slippage"); + + int128 E = numerator.div(denominator); + require(E > int128(0) && E < one, "test: E out of range"); + + int128 y = ABDKMath64x64.ln(E).neg().div(f); + require(y > int128(0), "test: y<=0"); + + return S.div(y); + } + // Helper function to update the state's cached qInternal function _updateCachedQInternal(int128[] memory mockQInternal) internal { // First ensure qInternal array exists with the right size @@ -955,7 +991,8 @@ contract LMSRStabilizedTest is Test { int128[] memory q = new int128[](2); q[0] = ABDKMath64x64.fromUInt(1_000_000); q[1] = ABDKMath64x64.fromUInt(1_000_000); - s.init(q, stdTradeSize, stdSlippage); + int128 bInit = _computeBFromSlippage(2, q, stdTradeSize, stdSlippage); + s.init(q, bInit); // Small trade (well within u <= 0.5 and delta <= 1%) int128 a = q[0].mul(ABDKMath64x64.divu(1, 1000)); // 0.1% of asset @@ -986,7 +1023,8 @@ contract LMSRStabilizedTest is Test { int128[] memory q = new int128[](2); q[0] = ABDKMath64x64.fromUInt(1_000_000); q[1] = ABDKMath64x64.fromUInt(1_000_000); - s.init(q, stdTradeSize, stdSlippage); + int128 bInit = _computeBFromSlippage(2, q, stdTradeSize, stdSlippage); + s.init(q, bInit); // Prepare newQ starting from equal quantities; we'll grow q0 until delta > DELTA_MAX int128[] memory newQ = new int128[](2); @@ -1045,7 +1083,8 @@ contract LMSRStabilizedTest is Test { int128[] memory q = new int128[](2); q[0] = ABDKMath64x64.fromUInt(1_000_000); q[1] = ABDKMath64x64.fromUInt(1_000_000); - s.init(q, stdTradeSize, stdSlippage); + int128 bInit = _computeBFromSlippage(2, q, stdTradeSize, stdSlippage); + s.init(q, bInit); // Compute b int128 b = _computeB(q); diff --git a/test/NativeTest.t.sol b/test/NativeTest.t.sol index 5d691e9..5a43a2a 100644 --- a/test/NativeTest.t.sol +++ b/test/NativeTest.t.sol @@ -53,6 +53,18 @@ contract NativeTest is Test { uint256 constant INIT_BAL = 1_000_000; // initial token units for each token uint256 constant BASE = 1; // use base=1 so internal amounts correspond to raw integers + // Compute fixed b for a pool initialized with numTokens tokens, each deposited with INIT_BAL (BASE == 1). + function _computeFixedB(uint256 numTokens) internal view returns (int128) { + int128 S = ABDKMath64x64.fromUInt(numTokens * INIT_BAL); + int128 one = ABDKMath64x64.fromInt(1); + int128 nMinus1 = ABDKMath64x64.fromUInt(numTokens - 1); + int128 numerator = one.sub(targetSlippage.mul(nMinus1)); + int128 denominator = one.add(targetSlippage); + int128 E = numerator.div(denominator); + int128 y = ABDKMath64x64.ln(E).neg().div(tradeFrac); + return S.div(y); + } + function setUp() public { alice = address(0xA11ce); bob = address(0xB0b); @@ -93,8 +105,8 @@ contract NativeTest is Test { // Deploy pool with a small fee (0.1%) uint256 feePpm = 1000; - int128 kappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); - pool = Deploy.newPartyPool(address(this), "LP", "LP", tokens, bases, kappa, feePpm, feePpm, weth, false); + int128 bFixed = _computeFixedB(tokens.length); + pool = Deploy.newPartyPool(address(this), "LP", "LP", tokens, bases, bFixed, feePpm, feePpm, weth, false); // Transfer initial deposit amounts into pool token0.transfer(address(pool), INIT_BAL); diff --git a/test/PartyPlanner.t.sol b/test/PartyPlanner.t.sol index d901450..670d265 100644 --- a/test/PartyPlanner.t.sol +++ b/test/PartyPlanner.t.sol @@ -15,6 +15,7 @@ import {LMSRStabilized} from "../src/LMSRStabilized.sol"; import {PartyPlanner} from "../src/PartyPlanner.sol"; import {PartyPool} from "../src/PartyPool.sol"; import {MockERC20} from "./PartyPlanner.t.sol"; +import "@abdk/ABDKMath64x64.sol"; // Mock ERC20 token for testing contract MockERC20 is ERC20 { @@ -34,11 +35,37 @@ contract MockERC20 is ERC20 { } contract PartyPlannerTest is Test { + using ABDKMath64x64 for int128; + PartyPlanner public planner; MockERC20 public tokenA; MockERC20 public tokenB; MockERC20 public tokenC; + function _computeFixedBFromDeposits( + uint256 nAssets, + uint256[] memory bases, + uint256[] memory initialDeposits, + int128 tradeFrac, + int128 targetSlippage + ) internal pure returns (int128) { + require(bases.length == initialDeposits.length && bases.length == nAssets, "length mismatch"); + // Compute S = sum(deposit_i / base_i) in Q64.64 + int128 S = 0; + for (uint256 i = 0; i < nAssets; i++) { + S = S + ABDKMath64x64.divu(initialDeposits[i], bases[i]); + } + // E = (1 - s*(n-1)) / (1 + s) + int128 one = ABDKMath64x64.fromInt(1); + int128 nMinus1 = ABDKMath64x64.fromUInt(nAssets - 1); + int128 numerator = one.sub(targetSlippage.mul(nMinus1)); + int128 denominator = one.add(targetSlippage); + int128 E = numerator.div(denominator); + // y = -ln(E) / f, b = S / y + int128 y = ABDKMath64x64.ln(E).neg().div(tradeFrac); + return S.div(y); + } + address public payer = makeAddr("payer"); address public receiver = makeAddr("receiver"); @@ -93,15 +120,15 @@ contract PartyPlannerTest is Test { uint256 initialTokenACount = planner.poolsByTokenCount(IERC20(address(tokenA))); uint256 initialTokenBCount = planner.poolsByTokenCount(IERC20(address(tokenB))); - // Compute kappa then create pool via kappa overload - int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); + // Compute fixed b from slippage and initial deposits, then create pool + int128 bFixed = _computeFixedBFromDeposits(tokens.length, bases, initialDeposits, tradeFrac, targetSlippage); (IPartyPool pool, uint256 lpAmount) = planner.newPool( name, symbol, tokens, bases, - computedKappa, + bFixed, swapFeePpm, flashFeePpm, false, // not stable @@ -174,10 +201,10 @@ contract PartyPlannerTest is Test { deposits1[0] = INITIAL_DEPOSIT_AMOUNT; deposits1[1] = INITIAL_DEPOSIT_AMOUNT; - int128 kappa1 = LMSRStabilized.computeKappaFromSlippage(tokens1.length, int128((1 << 64) - 1), int128(1 << 62)); + int128 b1 = _computeFixedBFromDeposits(tokens1.length, bases1, deposits1, int128((1 << 64) - 1), int128(1 << 62)); (IPartyPool pool1,) = planner.newPool( "Pool 1", "LP1", tokens1, bases1, - kappa1, 3000, 5000, false, + b1, 3000, 5000, false, payer, receiver, deposits1, 1000e18, 0 ); @@ -194,10 +221,10 @@ contract PartyPlannerTest is Test { deposits2[0] = INITIAL_DEPOSIT_AMOUNT; deposits2[1] = INITIAL_DEPOSIT_AMOUNT / 1e12; // Adjust for 6 decimals - int128 kappa2 = LMSRStabilized.computeKappaFromSlippage(tokens2.length, int128((1 << 64) - 1), int128(1 << 62)); + int128 b2 = _computeFixedBFromDeposits(tokens2.length, bases2, deposits2, int128((1 << 64) - 1), int128(1 << 62)); (IPartyPool pool2,) = planner.newPool( "Pool 2", "LP2", tokens2, bases2, - kappa2, 3000, 5000, false, + b2, 3000, 5000, false, payer, receiver, deposits2, 1000e18, 0 ); @@ -250,12 +277,12 @@ contract PartyPlannerTest is Test { validDeposits[0] = INITIAL_DEPOSIT_AMOUNT; validDeposits[1] = INITIAL_DEPOSIT_AMOUNT; - int128 kappaErr = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62)); + int128 bErr = _computeFixedBFromDeposits(tokens.length, bases, validDeposits, int128((1 << 64) - 1), int128(1 << 62)); vm.expectRevert("Planner: payer cannot be zero address"); planner.newPool( "Test Pool", "TESTLP", tokens, bases, - kappaErr, 3000, 5000, false, + bErr, 3000, 5000, false, address(0), receiver, validDeposits, 1000e18, 0 ); @@ -263,18 +290,18 @@ contract PartyPlannerTest is Test { vm.expectRevert("Planner: receiver cannot be zero address"); planner.newPool( "Test Pool", "TESTLP", tokens, bases, - kappaErr, 3000, 5000, false, + bErr, 3000, 5000, false, payer, address(0), validDeposits, 1000e18, 0 ); // Test deadline exceeded // The default timestamp is 1 and 1-0 is 0 which means "ignore deadline," so we need to set a proper timestamp. - int128 kappaDeadline = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62)); + int128 bDeadline = _computeFixedBFromDeposits(tokens.length, bases, validDeposits, int128((1 << 64) - 1), int128(1 << 62)); vm.warp(1000); vm.expectRevert("Planner: deadline exceeded"); planner.newPool( "Test Pool", "TESTLP", tokens, bases, - kappaDeadline, 3000, 5000, false, + bDeadline, 3000, 5000, false, payer, receiver, validDeposits, 1000e18, block.timestamp - 1 ); } @@ -297,12 +324,12 @@ contract PartyPlannerTest is Test { deposits[0] = INITIAL_DEPOSIT_AMOUNT; deposits[1] = INITIAL_DEPOSIT_AMOUNT; - int128 kappaLoop = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62)); + int128 bLoop = _computeFixedBFromDeposits(tokens.length, bases, deposits, int128((1 << 64) - 1), int128(1 << 62)); (IPartyPool pool,) = planner.newPool( string(abi.encodePacked("Pool ", vm.toString(i))), string(abi.encodePacked("LP", vm.toString(i))), tokens, bases, - kappaLoop, 3000, 5000, false, + bLoop, 3000, 5000, false, payer, receiver, deposits, 1000e18, 0 ); diff --git a/test/PartyPool.t.sol b/test/PartyPool.t.sol index 628928a..ff19e10 100644 --- a/test/PartyPool.t.sol +++ b/test/PartyPool.t.sol @@ -125,6 +125,18 @@ contract PartyPoolTest is Test { uint256 constant INIT_BAL = 1_000_000; // initial token units for each token (internal==amount when base==1) uint256 constant BASE = 1; // use base=1 so internal amounts correspond to raw integers (Q64.64 units) + // Compute fixed b for a pool initialized with numTokens tokens, each deposited with INIT_BAL (BASE == 1). + function _computeFixedB(uint256 numTokens) internal view returns (int128) { + int128 S = ABDKMath64x64.fromUInt(numTokens * INIT_BAL); + int128 one = ABDKMath64x64.fromInt(1); + int128 nMinus1 = ABDKMath64x64.fromUInt(numTokens - 1); + int128 numerator = one.sub(targetSlippage.mul(nMinus1)); + int128 denominator = one.add(targetSlippage); + int128 E = numerator.div(denominator); + int128 y = ABDKMath64x64.ln(E).neg().div(tradeFrac); + return S.div(y); + } + function setUp() public { planner = Deploy.newPartyPlanner(); alice = address(0xA11ce); @@ -172,8 +184,8 @@ contract PartyPoolTest is Test { // Deploy pool with a small fee to test fee-handling paths (use 1000 ppm = 0.1%) uint256 feePpm = 1000; - int128 kappa3 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); - pool = Deploy.newPartyPool(address(this), "LP", "LP", tokens, bases, kappa3, feePpm, feePpm, false); + int128 b3 = _computeFixedB(tokens.length); + pool = Deploy.newPartyPool(address(this), "LP", "LP", tokens, bases, b3, feePpm, feePpm, false); // Transfer initial deposit amounts into pool before initial mint (pool expects _tokens already in contract) // We deposit equal amounts INIT_BAL for each token @@ -202,8 +214,8 @@ contract PartyPoolTest is Test { bases10[i] = BASE; } - int128 kappa10 = LMSRStabilized.computeKappaFromSlippage(tokens10.length, tradeFrac, targetSlippage); - pool10 = Deploy.newPartyPool(address(this), "LP10", "LP10", tokens10, bases10, kappa10, feePpm, feePpm, false); + int128 b10 = _computeFixedB(tokens10.length); + pool10 = Deploy.newPartyPool(address(this), "LP10", "LP10", tokens10, bases10, b10, feePpm, feePpm, false); // Mint additional _tokens for pool10 initial deposit token0.mint(address(this), INIT_BAL); @@ -991,12 +1003,12 @@ contract PartyPoolTest is Test { uint256 feePpm = 1000; // Pool with default initialization (lpTokens = 0) - int128 kappaDefault = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); - PartyPool poolDefault = Deploy.newPartyPool(address(this), "LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault, feePpm, feePpm, false); + int128 bDefault = _computeFixedB(tokens.length); + PartyPool poolDefault = Deploy.newPartyPool(address(this), "LP_DEFAULT", "LP_DEFAULT", tokens, bases, bDefault, feePpm, feePpm, false); // Pool with custom initialization (lpTokens = custom amount) - int128 kappaCustom = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); - PartyPool poolCustom = Deploy.newPartyPool(address(this), "LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom, feePpm, feePpm, false); + int128 bCustom = _computeFixedB(tokens.length); + PartyPool poolCustom = Deploy.newPartyPool(address(this), "LP_CUSTOM", "LP_CUSTOM", tokens, bases, bCustom, feePpm, feePpm, false); // Mint additional _tokens for both pools token0.mint(address(this), INIT_BAL * 2); @@ -1067,10 +1079,10 @@ contract PartyPoolTest is Test { uint256 feePpm = 1000; - int128 kappaDefault2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); - PartyPool poolDefault = Deploy.newPartyPool(address(this), "LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault2, feePpm, feePpm, false); - int128 kappaCustom2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); - PartyPool poolCustom = Deploy.newPartyPool(address(this), "LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom2, feePpm, feePpm, false); + int128 bDefault2 = _computeFixedB(tokens.length); + PartyPool poolDefault = Deploy.newPartyPool(address(this), "LP_DEFAULT", "LP_DEFAULT", tokens, bases, bDefault2, feePpm, feePpm, false); + int128 bCustom2 = _computeFixedB(tokens.length); + PartyPool poolCustom = Deploy.newPartyPool(address(this), "LP_CUSTOM", "LP_CUSTOM", tokens, bases, bCustom2, feePpm, feePpm, false); // Mint additional _tokens token0.mint(address(this), INIT_BAL * 4);