From 5a2e7039d188710308c0bee38bb1f07db4a0f130 Mon Sep 17 00:00:00 2001 From: tim Date: Wed, 1 Oct 2025 17:08:02 -0400 Subject: [PATCH] LMSRStabilized pure refactor; swapMintAmounts --- src/LMSRStabilized.sol | 248 ++++++++++++++++++++++++++++------ src/PartyPool.sol | 65 ++++----- src/PartyPoolMintImpl.sol | 44 ++++-- src/PartyPoolSwapMintImpl.sol | 147 ++++++++++++++++++++ 4 files changed, 417 insertions(+), 87 deletions(-) diff --git a/src/LMSRStabilized.sol b/src/LMSRStabilized.sol index bb55f1a..afbc0fd 100644 --- a/src/LMSRStabilized.sol +++ b/src/LMSRStabilized.sol @@ -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++; } } diff --git a/src/PartyPool.sol b/src/PartyPool.sol index c0d8832..3bf2a61 100644 --- a/src/PartyPool.sol +++ b/src/PartyPool.sol @@ -128,37 +128,14 @@ contract PartyPool is PartyPoolBase, IPartyPool { /// @inheritdoc IPartyPool function initialMint(address receiver, uint256 lpTokens) external nonReentrant returns (uint256 lpMinted) { - uint256 n = tokens.length; - - // Check if this is initial deposit - revert if not - bool isInitialDeposit = totalSupply() == 0 || lmsr.nAssets == 0; - require(isInitialDeposit, "initialMint: pool already initialized"); - - // Update cached balances for all assets - int128[] memory newQInternal = new int128[](n); - uint256[] memory depositAmounts = new uint256[](n); - for (uint i = 0; i < n; ) { - uint256 bal = IERC20(tokens[i]).balanceOf(address(this)); - cachedUintBalances[i] = bal; - newQInternal[i] = _uintToInternalFloor(bal, bases[i]); - depositAmounts[i] = bal; - unchecked { i++; } - } - - // Initialize the stabilized LMSR state with provided kappa - lmsr.init(newQInternal, KAPPA); - - // Compute actual LP tokens to mint based on size metric (scaled) - if( lpTokens != 0 ) - lpMinted = lpTokens; - else { - int128 newTotal = _computeSizeMetric(newQInternal); - lpMinted = ABDKMath64x64.mulu(newTotal, LP_SCALE); - } - - require(lpMinted > 0, "initialMint: zero LP amount"); - _mint(receiver, lpMinted); - emit Mint(address(0), receiver, depositAmounts, lpMinted); + bytes memory data = abi.encodeWithSignature( + "initialMint(address,uint256,int128)", + receiver, + lpTokens, + KAPPA + ); + bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data); + return abi.decode(result, (uint256)); } /// @inheritdoc IPartyPool @@ -460,11 +437,29 @@ contract PartyPool is PartyPoolBase, IPartyPool { return netUint + fee; } - // --- New events for single-token mint/burn flows --- - // Note: events intentionally avoid exposing internal ΔS and avoid duplicating LP mint/burn data - // which is already present in the standard Mint/Burn events. + function swapMintAmounts(uint256 inputTokenIndex, uint256 maxAmountIn) external view + returns (uint256 amountInUsed, uint256 fee, uint256 lpMinted) { + return SWAP_MINT_IMPL.swapMintAmounts( + inputTokenIndex, + maxAmountIn, + SWAP_FEE_PPM, + lmsr, + bases, + totalSupply() + ); + } - // todo swapMintAmounts and burnSwapAmounts + function burnSwapAmounts(uint256 lpAmount, uint256 inputTokenIndex) external view + returns (uint256 amountOut) { + return SWAP_MINT_IMPL.burnSwapAmounts( + lpAmount, + inputTokenIndex, + SWAP_FEE_PPM, + lmsr, + bases, + totalSupply() + ); + } /// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP. /// @dev This function forwards the call to the swapMint implementation via delegatecall diff --git a/src/PartyPoolMintImpl.sol b/src/PartyPoolMintImpl.sol index a1f414d..9afbcae 100644 --- a/src/PartyPoolMintImpl.sol +++ b/src/PartyPoolMintImpl.sol @@ -6,6 +6,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./PartyPoolBase.sol"; import "./LMSRStabilized.sol"; +import {PartyPool} from "./PartyPool.sol"; /// @title PartyPoolMintImpl - Implementation contract for mint and burn functions /// @notice This contract contains the mint and burn implementation that will be called via delegatecall @@ -21,14 +22,41 @@ contract PartyPoolMintImpl is PartyPoolBase { constructor() PartyPoolBase('','') {} - /// @notice Proportional mint for existing pool. - /// @dev Payer must approve the required token amounts before calling. - /// Can only be called when pool is already initialized (totalSupply() > 0 and lmsr.nAssets > 0). - /// Rounds follow the pool-favorable conventions documented in helpers (ceil inputs, floor outputs). - /// @param payer address that provides the input tokens - /// @param receiver address that receives the LP tokens - /// @param lpTokenAmount desired amount of LP tokens to mint - /// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore. + function initialMint(address receiver, uint256 lpTokens, int128 KAPPA) external + returns (uint256 lpMinted) { + uint256 n = tokens.length; + + // Check if this is initial deposit - revert if not + bool isInitialDeposit = totalSupply() == 0 || lmsr.nAssets == 0; + require(isInitialDeposit, "initialMint: pool already initialized"); + + // Update cached balances for all assets + int128[] memory newQInternal = new int128[](n); + uint256[] memory depositAmounts = new uint256[](n); + for (uint i = 0; i < n; ) { + uint256 bal = IERC20(tokens[i]).balanceOf(address(this)); + cachedUintBalances[i] = bal; + newQInternal[i] = _uintToInternalFloor(bal, bases[i]); + depositAmounts[i] = bal; + unchecked { i++; } + } + + // Initialize the stabilized LMSR state with provided kappa + lmsr.init(newQInternal, KAPPA); + + // Compute actual LP tokens to mint based on size metric (scaled) + if( lpTokens != 0 ) + lpMinted = lpTokens; + else { + int128 newTotal = _computeSizeMetric(newQInternal); + lpMinted = ABDKMath64x64.mulu(newTotal, LP_SCALE); + } + + require(lpMinted > 0, "initialMint: zero LP amount"); + _mint(receiver, lpMinted); + emit Mint(address(0), receiver, depositAmounts, lpMinted); + } + function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external returns (uint256 lpMinted) { require(deadline == 0 || block.timestamp <= deadline, "mint: deadline exceeded"); uint256 n = tokens.length; diff --git a/src/PartyPoolSwapMintImpl.sol b/src/PartyPoolSwapMintImpl.sol index 1d5249f..0db1d6d 100644 --- a/src/PartyPoolSwapMintImpl.sol +++ b/src/PartyPoolSwapMintImpl.sol @@ -125,6 +125,117 @@ contract PartyPoolSwapMintImpl is PartyPoolBase { return actualLpToMint; } + /// @notice Calculate the amounts for a swap mint operation + /// @dev This is a pure view function that computes swap mint amounts from provided state + /// @param inputTokenIndex index of the input token + /// @param maxAmountIn maximum amount of token to deposit (inclusive of fee) + /// @param swapFeePpm fee in parts-per-million + /// @param lmsrState current LMSR state + /// @param bases_ scaling bases for each token + /// @param totalSupply_ current total LP token supply + /// @return amountInUsed actual input amount used (excluding fee) + /// @return fee fee amount charged + /// @return lpMinted LP tokens that would be minted + function swapMintAmounts( + uint256 inputTokenIndex, + uint256 maxAmountIn, + uint256 swapFeePpm, + LMSRStabilized.State memory lmsrState, + uint256[] memory bases_, + uint256 totalSupply_ + ) public pure returns (uint256 amountInUsed, uint256 fee, uint256 lpMinted) { + require(inputTokenIndex < bases_.length, "swapMintAmounts: idx"); + require(maxAmountIn > 0, "swapMintAmounts: input zero"); + require(lmsrState.nAssets > 0, "swapMintAmounts: uninit pool"); + + // Compute fee on gross maxAmountIn to get an initial net estimate + uint256 feeGuess = 0; + uint256 netUintGuess = maxAmountIn; + if (swapFeePpm > 0) { + feeGuess = (maxAmountIn * swapFeePpm + 999999) / 1000000; // ceil fee + netUintGuess = maxAmountIn - feeGuess; + } + + // Convert the net guess to internal (floor) + int128 netInternalGuess = _uintToInternalFloorPure(netUintGuess, bases_[inputTokenIndex]); + require(netInternalGuess > int128(0), "swapMintAmounts: input too small after fee"); + + // 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, + inputTokenIndex, netInternalGuess); + + // amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer + amountInUsed = _internalToUintCeilPure(amountInInternalUsed, bases_[inputTokenIndex]); + require(amountInUsed > 0, "swapMintAmounts: input zero after internal conversion"); + + // Compute fee on the actual used input (ceiling) + fee = 0; + if (swapFeePpm > 0) { + fee = (amountInUsed * swapFeePpm + 999999) / 1000000; // ceil fee + } + uint256 totalTransfer = amountInUsed + fee; + require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMintAmounts: transfer exceeds max"); + + // Compute old and new scaled size metrics to determine LP minted + int128 oldTotal = _computeSizeMetricPure(lmsrState.qInternal); + require(oldTotal > int128(0), "swapMintAmounts: zero total"); + uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE); + + int128 newTotal = oldTotal.add(sizeIncreaseInternal); + uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE); + + if (totalSupply_ == 0) { + // If somehow supply zero (shouldn't happen as lmsr.nAssets>0), mint newScaled + lpMinted = newScaled; + } else { + require(oldScaled > 0, "swapMintAmounts: oldScaled zero"); + uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0; + if (delta > 0) { + // floor truncation rounds in favor of pool + lpMinted = (totalSupply_ * delta) / oldScaled; + } else { + lpMinted = 0; + } + } + + require(lpMinted > 0, "swapMintAmounts: zero LP minted"); + } + + /// @notice Calculate the amounts for a burn swap operation + /// @dev This is a pure view function that computes burn swap amounts from provided state + /// @param lpAmount amount of LP tokens to burn + /// @param inputTokenIndex index of target asset to receive + /// @param swapFeePpm fee in parts-per-million + /// @param lmsrState current LMSR state + /// @param bases_ scaling bases for each token + /// @param totalSupply_ current total LP token supply + /// @return amountOut amount of target asset that would be received + function burnSwapAmounts( + uint256 lpAmount, + uint256 inputTokenIndex, + uint256 swapFeePpm, + LMSRStabilized.State memory lmsrState, + uint256[] memory bases_, + uint256 totalSupply_ + ) public pure returns (uint256 amountOut) { + require(inputTokenIndex < bases_.length, "burnSwapAmounts: idx"); + require(lpAmount > 0, "burnSwapAmounts: zero lp"); + require(totalSupply_ > 0, "burnSwapAmounts: empty supply"); + + // alpha = lpAmount / supply as Q64.64 + int128 alpha = ABDKMath64x64.divu(lpAmount, totalSupply_) // fraction of total supply to burn + .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, + inputTokenIndex, alpha); + + // Convert payoutInternal -> uint (floor) to favor pool + amountOut = _internalToUintFloorPure(payoutInternal, bases_[inputTokenIndex]); + require(amountOut > 0, "burnSwapAmounts: output zero"); + } + /// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver. /// @dev The function burns LP tokens (authorization via allowance if needed), sends the single-asset payout and updates LMSR state. /// @param payer who burns LP tokens @@ -198,4 +309,40 @@ contract PartyPoolSwapMintImpl is PartyPoolBase { emit Burn(payer, receiver, new uint256[](n), lpAmount); return amountOutUint; } + + /// @notice Pure version of _uintToInternalFloor for use in view functions + function _uintToInternalFloorPure(uint256 amount, uint256 base) internal pure returns (int128) { + // amount / base as Q64.64, floored + return ABDKMath64x64.divu(amount, base); + } + + /// @notice Pure version of _internalToUintCeil for use in view functions + function _internalToUintCeilPure(int128 amount, uint256 base) internal pure returns (uint256) { + // Convert Q64.64 to uint with ceiling: ceil(amount * base) + // Use mulu which floors, then add remainder check for ceiling + uint256 floored = ABDKMath64x64.mulu(amount, base); + // Check if there's a fractional part by computing amount * base - floored + int128 baseQ64 = ABDKMath64x64.fromUInt(base); + int128 flooredQ64 = ABDKMath64x64.fromUInt(floored); + int128 product = amount.mul(baseQ64); + if (product > flooredQ64) { + return floored + 1; // Ceiling + } + return floored; + } + + /// @notice Pure version of _internalToUintFloor for use in view functions + function _internalToUintFloorPure(int128 amount, uint256 base) internal pure returns (uint256) { + // Convert Q64.64 to uint with floor: floor(amount * base) + return ABDKMath64x64.mulu(amount, base); + } + + /// @notice Pure version of _computeSizeMetric for use in view functions + function _computeSizeMetricPure(int128[] memory qInternal) internal pure returns (int128) { + int128 sum = int128(0); + for (uint256 i = 0; i < qInternal.length; i++) { + sum = sum.add(qInternal[i]); + } + return sum; + } }