LMSRStabilized pure refactor; swapMintAmounts

This commit is contained in:
tim
2025-10-01 17:08:02 -04:00
parent a6f6fd034c
commit 5a2e7039d1
4 changed files with 417 additions and 87 deletions

View File

@@ -64,8 +64,15 @@ library LMSRStabilized {
/// @notice Cost C(q) = b * (M + ln(Z))
function cost(State storage s) internal view returns (int128) {
int128 b = _computeB(s);
(int128 M, int128 Z) = _computeMAndZ(b, s.qInternal);
return cost(s.kappa, 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);
(int128 M, int128 Z) = _computeMAndZ(b, qInternal);
int128 lnZ = _ln(Z);
int128 inner = M.add(lnZ);
int128 c = b.mul(inner);
@@ -102,23 +109,58 @@ library LMSRStabilized {
int128 a,
int128 limitPrice
) internal view returns (int128 amountIn, int128 amountOut) {
require(i < s.nAssets && j < s.nAssets, "LMSR: idx");
return swapAmountsForExactInput(s.nAssets, s.kappa, 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)
/// Uses the closed-form two-asset LMSR formula (no fees in kernel):
/// y = b * ln(1 + r0 * (1 - exp(-a / b)))
/// where r0 = e_i / e_j.
///
/// This variant accepts an additional `limitPrice` (64.64) which represents the
/// maximum acceptable marginal price (p_i / p_j). If the marginal price would
/// exceed `limitPrice` before the requested `a` is fully consumed, the input
/// `a` is truncated to the value that makes the marginal price equal `limitPrice`.
///
/// 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 qInternal Cached internal balances in 64.64 fixed-point format
/// @param i Index of input asset
/// @param j Index of output asset
/// @param a Amount of input asset (in int128 format, 64.64 fixed-point)
/// @param limitPrice Maximum acceptable price ratio (64.64). If <= current price, this call reverts.
/// @return amountIn Actual amount of input asset used (may be less than `a` if limited by price)
/// @return amountOut Amount of output asset j in 64.64 fixed-point format
function swapAmountsForExactInput(
uint256 nAssets,
int128 kappa,
int128[] memory qInternal,
uint256 i,
uint256 j,
int128 a,
int128 limitPrice
) internal pure returns (int128 amountIn, int128 amountOut) {
require(i < nAssets && j < nAssets, "LMSR: idx");
// Initialize amountIn to full amount (will be adjusted if limit price is hit)
amountIn = a;
// Compute b and ensure positivity before deriving invB
int128 b = _computeB(s);
int128 sizeMetric = _computeSizeMetric(qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
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(s.qInternal[j] > int128(0), "LMSR: e_j==0");
require(qInternal[j] > int128(0), "LMSR: e_j==0");
// Compute r0 = exp((q_i - q_j) / b) directly using invB
int128 r0 = _exp(s.qInternal[i].sub(s.qInternal[j]).mul(invB));
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
@@ -203,7 +245,7 @@ library LMSRStabilized {
// If inner <= 0 then cap output to the current balance q_j (cannot withdraw more than q_j)
if (inner <= int128(0)) {
console2.log("WARNING: inner <= 0, capping output to balance q_j");
int128 qj64 = s.qInternal[j];
int128 qj64 = qInternal[j];
console2.log("Capped output (64.64):");
console2.logInt(qj64);
return (amountIn, qj64);
@@ -249,21 +291,48 @@ library LMSRStabilized {
uint256 j,
int128 limitPrice
) internal view returns (int128 amountIn, int128 amountOut) {
require(i < s.nAssets && j < s.nAssets, "LMSR: idx");
return swapAmountsForPriceLimit(s.nAssets, s.kappa, s.qInternal, i, j, limitPrice);
}
/// @notice Pure version: Maximum input/output pair possible when swapping from asset i to asset j
/// given a maximum acceptable price ratio (p_i/p_j).
/// Returns the input amount that would drive the marginal price to the limit (amountIn)
/// and the corresponding output amount (amountOut). If the output would exceed the
/// 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 qInternal Cached internal balances in 64.64 fixed-point format
/// @param i Index of input asset
/// @param j Index of output asset
/// @param limitPrice Maximum acceptable price ratio (64.64)
/// @return amountIn Maximum input amount in 64.64 fixed-point format that reaches the price limit
/// @return amountOut Corresponding maximum output amount in 64.64 fixed-point format
function swapAmountsForPriceLimit(
uint256 nAssets,
int128 kappa,
int128[] memory qInternal,
uint256 i,
uint256 j,
int128 limitPrice
) internal pure returns (int128 amountIn, int128 amountOut) {
require(i < nAssets && j < nAssets, "LMSR: idx");
require(limitPrice > int128(0), "LMSR: limitPrice <= 0");
// Compute b and ensure positivity before deriving invB
int128 b = _computeB(s);
int128 sizeMetric = _computeSizeMetric(qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
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(s.qInternal[j] > int128(0), "LMSR: e_j==0");
require(qInternal[j] > int128(0), "LMSR: e_j==0");
// Compute r0 = exp((q_i - q_j) / b) directly using invB
int128 r0 = _exp(s.qInternal[i].sub(s.qInternal[j]).mul(invB));
int128 r0 = _exp(qInternal[i].sub(qInternal[j]).mul(invB));
console2.log("\n=== Max Input/Output Calculation ===");
console2.log("Limit price (64x64):");
@@ -316,7 +385,7 @@ library LMSRStabilized {
console2.logInt(maxOutput);
// Current balance of asset j (in 64.64)
int128 qj64 = s.qInternal[j];
int128 qj64 = qInternal[j];
console2.log("Current j balance (64.64):");
console2.logInt(qj64);
@@ -366,20 +435,45 @@ library LMSRStabilized {
uint256 i,
int128 a
) internal view returns (int128 amountIn, int128 amountOut) {
require(i < s.nAssets, "LMSR: idx");
return swapAmountsForMint(s.nAssets, s.kappa, s.qInternal, i, a);
}
/// @notice Pure version: Compute LP-size increase when minting from a single-token input using bisection only.
/// @dev Solve for α >= 0 such that:
/// a = α*q_i + sum_{j != i} x_j(α)
/// where x_j(α) is the input to swap i->j that yields y_j = α*q_j and
/// 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 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)
/// @return amountIn Actual amount of input consumed
/// @return amountOut LP size-metric increase (alpha * S)
function swapAmountsForMint(
uint256 nAssets,
int128 kappa,
int128[] memory qInternal,
uint256 i,
int128 a
) internal pure returns (int128 amountIn, int128 amountOut) {
require(i < nAssets, "LMSR: idx");
require(a > int128(0), "LMSR: amount <= 0");
int128 b = _computeB(s);
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);
int128 S = _computeSizeMetric(s.qInternal);
int128 S = sizeMetric;
uint256 n = s.nAssets;
uint256 n = nAssets;
// Precompute r0_j = exp((q_i - q_j) / b) for all j to avoid recomputing during search.
int128[] memory r0 = new int128[](n);
for (uint256 j = 0; j < n; ) {
r0[j] = _exp(s.qInternal[i].sub(s.qInternal[j]).mul(invB));
r0[j] = _exp(qInternal[i].sub(qInternal[j]).mul(invB));
unchecked { j++; }
}
@@ -415,7 +509,7 @@ library LMSRStabilized {
// loop j != i
for (uint256 j = 0; j < n; ) {
if (j != i) {
int128 yj = alpha.mul(s.qInternal[j]); // target output y_j = alpha * q_j
int128 yj = alpha.mul(qInternal[j]); // target output y_j = alpha * q_j
if (yj > int128(0)) {
int128 expArg = yj.mul(invB);
// Guard exp arg
@@ -433,7 +527,7 @@ library LMSRStabilized {
unchecked { j++; }
}
int128 aReq = fail ? int128(type(int128).max) : alpha.mul(s.qInternal[i]).add(sumX);
int128 aReq = fail ? int128(type(int128).max) : alpha.mul(qInternal[i]).add(sumX);
if (aReq >= a || high >= alphaCap) {
break;
@@ -455,7 +549,7 @@ library LMSRStabilized {
for (uint256 j = 0; j < n; ) {
if (j != i) {
int128 yj = alpha.mul(s.qInternal[j]);
int128 yj = alpha.mul(qInternal[j]);
if (yj > int128(0)) {
int128 expArg = yj.mul(invB);
if (expArg > EXP_LIMIT) { fail = true; break; }
@@ -472,7 +566,7 @@ library LMSRStabilized {
unchecked { j++; }
}
int128 aReq = fail ? int128(type(int128).max) : alpha.mul(s.qInternal[i]).add(sumX);
int128 aReq = fail ? int128(type(int128).max) : alpha.mul(qInternal[i]).add(sumX);
if (aReq > a) {
// mid requires more input than provided -> decrease alpha
@@ -502,7 +596,7 @@ library LMSRStabilized {
bool failFinal = false;
for (uint256 j = 0; j < n; ) {
if (j != i) {
int128 yj = alphaFinal.mul(s.qInternal[j]);
int128 yj = alphaFinal.mul(qInternal[j]);
if (yj > int128(0)) {
int128 expArg = yj.mul(invB);
if (expArg > EXP_LIMIT) { failFinal = true; break; }
@@ -524,7 +618,7 @@ library LMSRStabilized {
return (int128(0), int128(0));
}
int128 aRequired = alphaFinal.mul(s.qInternal[i]).add(sumXFinal);
int128 aRequired = alphaFinal.mul(qInternal[i]).add(sumXFinal);
// amountIn is actual consumed input (may be <= provided a)
amountIn = aRequired;
@@ -553,28 +647,56 @@ library LMSRStabilized {
uint256 i,
int128 alpha
) internal view returns (int128 amountOut, int128 amountIn) {
require(i < s.nAssets, "LMSR: idx");
return swapAmountsForBurn(s.nAssets, s.kappa, s.qInternal, i, alpha);
}
/// @notice Pure version: Compute single-asset payout when burning a proportional share alpha of the pool.
/// @dev Simulate q_after = (1 - alpha) * q, return the amount of asset `i` the burner
/// would receive after swapping each other asset's withdrawn portion into `i`.
/// For each j != i:
/// - wrapper holds a_j = alpha * q_j
/// - swap j->i with closed-form exact-input formula using the current q_local
/// - cap output to q_local[i] when necessary (solve inverse for input used)
/// 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 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)
/// @return amountOut Amount of asset i received (in 64.64 fixed-point)
/// @return amountIn LP size-metric redeemed (alpha * S)
function swapAmountsForBurn(
uint256 nAssets,
int128 kappa,
int128[] memory qInternal,
uint256 i,
int128 alpha
) internal pure returns (int128 amountOut, int128 amountIn) {
require(i < nAssets, "LMSR: idx");
require(alpha > int128(0) && alpha <= ONE, "LMSR: alpha");
int128 b = _computeB(s);
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);
uint256 n = s.nAssets;
uint256 n = nAssets;
// Size metric and burned size (amountIn returned)
int128 S = _computeSizeMetric(s.qInternal);
int128 S = sizeMetric;
amountIn = alpha.mul(S); // total size-metric redeemed
// Build q_local := q_after_burn = (1 - alpha) * q
int128[] memory qLocal = new int128[](n);
for (uint256 j = 0; j < n; ) {
qLocal[j] = s.qInternal[j].mul(ONE.sub(alpha));
qLocal[j] = qInternal[j].mul(ONE.sub(alpha));
unchecked { j++; }
}
// Start totalOut with direct portion of asset i redeemed
int128 totalOut = alpha.mul(s.qInternal[i]);
int128 totalOut = alpha.mul(qInternal[i]);
// Track whether any non-zero contribution was produced
bool anyNonZero = (totalOut > int128(0));
@@ -582,7 +704,7 @@ library LMSRStabilized {
// For each asset j != i, swap the withdrawn a_j := alpha * q_j into i
for (uint256 j = 0; j < n; ) {
if (j != i) {
int128 aj = alpha.mul(s.qInternal[j]); // wrapper-held withdrawn amount of j
int128 aj = alpha.mul(qInternal[j]); // wrapper-held withdrawn amount of j
if (aj > int128(0)) {
// expArg = aj / b
int128 expArg = aj.mul(invB);
@@ -722,15 +844,26 @@ library LMSRStabilized {
/// @notice Price-share of asset i: exp(z_i) / Z (64.64)
function priceShare(State storage s, uint256 i) internal view returns (int128) {
int128 b = _computeB(s);
uint len = s.qInternal.length;
return priceShare(s.kappa, 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 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);
uint len = qInternal.length;
require(len > 0, "LMSR: no assets");
// Precompute reciprocal of b and perform a single pass that tracks M, Z, and e_i
int128 invB = ABDKMath64x64.div(ONE, b);
// Initialize from the first element
int128 M = s.qInternal[0].mul(invB);
int128 M = qInternal[0].mul(invB);
int128 Z = ONE; // exp(0)
int128 e_i_acc;
bool setEi;
@@ -741,7 +874,7 @@ library LMSRStabilized {
}
for (uint idx = 1; idx < len; ) {
int128 yi = s.qInternal[idx].mul(invB);
int128 yi = qInternal[idx].mul(invB);
if (yi <= M) {
// Add contribution under current center
int128 term = _exp(yi.sub(M));
@@ -770,7 +903,7 @@ library LMSRStabilized {
if (!setEi) {
// Only possible when len == 1 and i != 0, guarded by caller invariants typically
// Fallback: compute directly (kept for completeness)
int128 yi = s.qInternal[i].mul(invB);
int128 yi = qInternal[i].mul(invB);
e_i_acc = _exp(yi.sub(M));
}
@@ -780,27 +913,54 @@ 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) {
require(baseTokenIndex < s.nAssets && quoteTokenIndex < s.nAssets, "LMSR: idx");
int128 b = _computeB(s);
return price(s.nAssets, s.kappa, 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 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) {
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
int128 invB = ABDKMath64x64.div(ONE, b);
// Marginal price p_quote / p_base = exp((q_quote - q_base) / b)
return _exp(s.qInternal[quoteTokenIndex].sub(s.qInternal[baseTokenIndex]).mul(invB));
return _exp(qInternal[quoteTokenIndex].sub(qInternal[baseTokenIndex]).mul(invB));
}
/// @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) {
require(quoteTokenIndex < s.nAssets, "LMSR: idx");
return poolPrice(s.nAssets, s.kappa, 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 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) {
require(quoteTokenIndex < nAssets, "LMSR: idx");
// Compute b and ensure positivity
int128 b = _computeB(s);
int128 sizeMetric = _computeSizeMetric(qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
require(b > int128(0), "LMSR: b<=0");
// Compute total size metric S = sum q_i
int128 S = _computeSizeMetric(s.qInternal);
int128 S = sizeMetric;
require(S > int128(0), "LMSR: size zero");
// Precompute reciprocal of b
@@ -808,12 +968,12 @@ library LMSRStabilized {
// Accumulate weighted exponentials: sum_j q_j * exp((q_j - q_quote) / b)
int128 acc = int128(0);
uint256 n = s.nAssets;
uint256 n = nAssets;
for (uint256 j = 0; j < n; ) {
// factor = exp((q_j - q_quote) / b)
int128 factor = _exp(s.qInternal[j].sub(s.qInternal[quoteTokenIndex]).mul(invB));
int128 factor = _exp(qInternal[j].sub(qInternal[quoteTokenIndex]).mul(invB));
// term = q_j * factor
int128 term = s.qInternal[j].mul(factor);
int128 term = qInternal[j].mul(factor);
acc = acc.add(term);
unchecked { j++; }
}