1 Commits

Author SHA1 Message Date
tim
96dc134769 fixed-b 2025-10-24 20:01:24 -04:00
11 changed files with 421 additions and 253 deletions

View File

@@ -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.

View File

@@ -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");
}
// 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");
}
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;
}
}

View File

@@ -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));

View File

@@ -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

View File

@@ -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)

View File

@@ -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());
}

View File

@@ -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++) {

View File

@@ -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);

View File

@@ -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);

View File

@@ -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
);

View File

@@ -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);