diff --git a/src/IPartyPool.sol b/src/IPartyPool.sol index 868ad29..1801c53 100644 --- a/src/IPartyPool.sol +++ b/src/IPartyPool.sol @@ -188,6 +188,15 @@ interface IPartyPool is IERC20Metadata { uint256 deadline ) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee); + /// @notice External view to quote swapMint amounts, matching swapMint() computations + /// @param inputTokenIndex index of input token to deposit + /// @param maxAmountIn maximum gross input allowed (inclusive of fee) + /// @return totalTransfer gross input amount to transfer (includes fee), amountIn net input amount used for minting, fee fee amount taken, lpMinted LP tokens that would be minted + function swapMintAmounts( + uint256 inputTokenIndex, + uint256 maxAmountIn + ) external view returns (uint256 totalTransfer, uint256 amountIn, uint256 fee, uint256 lpMinted); + /// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP. /// @dev swapMint executes as an exact-in planned swap followed by proportional scaling of qInternal. /// The function emits SwapMint (gross, net, fee) and also emits Mint for LP issuance. @@ -205,6 +214,15 @@ interface IPartyPool is IERC20Metadata { uint256 deadline ) external returns (uint256 lpMinted); + /// @notice External view to quote burnSwap amounts, matching burnSwap() computations + /// @param lpAmount amount of LP tokens to burn + /// @param inputTokenIndex index of target asset to receive + /// @return amountOut output amount user would receive after fees + function burnSwapAmounts( + uint256 lpAmount, + uint256 inputTokenIndex + ) external view returns (uint256 amountOut); + /// @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 diff --git a/src/PartyPool.sol b/src/PartyPool.sol index aa2107f..98c7bb4 100644 --- a/src/PartyPool.sol +++ b/src/PartyPool.sol @@ -170,6 +170,14 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { return s.swapToLimitAmounts(inputTokenIndex, outputTokenIndex, limitPrice, swapFeePpm); } + /// @inheritdoc IPartyPool + function swapMintAmounts( + uint256 inputTokenIndex, + uint256 maxAmountIn + ) external view returns (uint256 totalTransfer, uint256 amountIn, uint256 fee, uint256 lpMinted) { + return s.swapMintAmounts(inputTokenIndex, maxAmountIn, swapFeePpm, totalSupply()); + } + /// @notice Swap input token i -> token j. Payer must approve token i. /// @dev This function transfers the exact gross input (including fee) from payer and sends the computed output to receiver. /// Non-standard tokens (fee-on-transfer, rebasers) are rejected via balance checks. @@ -228,6 +236,14 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { _mint(receiver, lpMinted); } + /// @inheritdoc IPartyPool + function burnSwapAmounts( + uint256 lpAmount, + uint256 inputTokenIndex + ) external view returns (uint256 amountOut) { + return s.burnSwapAmounts(lpAmount, inputTokenIndex, swapFeePpm, totalSupply()); + } + /// @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 diff --git a/src/PoolLib.sol b/src/PoolLib.sol index cb257dd..b41d28c 100644 --- a/src/PoolLib.sol +++ b/src/PoolLib.sol @@ -321,6 +321,64 @@ library PoolLib { return (grossIn, outUint, feeUint); } + /// @notice Get amounts for swapMint operation + function swapMintAmounts( + State storage state, + uint256 inputTokenIndex, + uint256 maxAmountIn, + uint256 swapFeePpm, + uint256 totalSupply + ) internal view returns (uint256 totalTransfer, uint256 amountIn, uint256 fee, uint256 lpMinted) { + uint256 n = state.tokens.length; + require(inputTokenIndex < n, "swapMintAmounts: idx"); + require(maxAmountIn > 0, "swapMintAmounts: input zero"); + require(state.lmsr.nAssets > 0, "swapMintAmounts: uninit pool"); + + // Compute fee on gross maxAmountIn to get initial net estimate + (, uint256 netUintGuess) = _computeFee(maxAmountIn, swapFeePpm); + + // Convert the net guess to internal (floor) + int128 netInternalGuess = _uintToInternalFloor(netUintGuess, state.bases[inputTokenIndex]); + require(netInternalGuess > int128(0), "swapMintAmounts: input too small after fee"); + + // Use LMSR view to determine actual internal consumed and size-increase + (int128 amountInInternalUsed, int128 sizeIncreaseInternal) = state.lmsr.swapAmountsForMint(inputTokenIndex, netInternalGuess); + + // Convert to uint (ceil) to determine actual transfer + uint256 amountInUint = _internalToUintCeil(amountInInternalUsed, state.bases[inputTokenIndex]); + require(amountInUint > 0, "swapMintAmounts: input zero after internal conversion"); + + // Compute fee on actual used input and total transfer amount + uint256 feeUintActual = _ceilFee(amountInUint, swapFeePpm); + uint256 totalTransferAmount = amountInUint + feeUintActual; + require(totalTransferAmount > 0 && totalTransferAmount <= maxAmountIn, "swapMintAmounts: transfer exceeds max"); + + // Compute old and new scaled size metrics + int128 oldTotal = _computeSizeMetric(state.lmsr.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); + + uint256 actualLpToMint; + if (totalSupply == 0) { + actualLpToMint = newScaled; + } else { + require(oldScaled > 0, "swapMintAmounts: oldScaled zero"); + uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0; + if (delta > 0) { + actualLpToMint = (totalSupply * delta) / oldScaled; + } else { + actualLpToMint = 0; + } + } + + require(actualLpToMint > 0, "swapMintAmounts: zero LP minted"); + + return (totalTransferAmount, amountInUint, feeUintActual, actualLpToMint); + } + /// @notice Execute exact input swap function swap( State storage state, @@ -415,6 +473,39 @@ library PoolLib { return (amountInUsedUint, amountOutUint, feeUint); } + /// @notice Get amounts for burnSwap operation + function burnSwapAmounts( + State storage state, + uint256 lpAmount, + uint256 inputTokenIndex, + uint256 swapFeePpm, + uint256 totalSupply + ) internal view returns (uint256 amountOut) { + uint256 n = state.tokens.length; + require(inputTokenIndex < n, "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); + + // Use LMSR view to compute single-asset payout + (int128 payoutInternal, ) = state.lmsr.swapAmountsForBurn(inputTokenIndex, alpha); + + // Convert payoutInternal -> uint (floor) to favor pool + uint256 amountOutUint = _internalToUintFloor(payoutInternal, state.bases[inputTokenIndex]); + require(amountOutUint > 0, "burnSwapAmounts: output zero"); + + // Apply swap fee to the output + if (swapFeePpm > 0) { + uint256 feeUint = _ceilFee(amountOutUint, swapFeePpm); + require(amountOutUint > feeUint, "burnSwapAmounts: fee exceeds output"); + amountOutUint -= feeUint; + } + + return amountOutUint; + } + /// @notice Single-token mint (swapMint) function swapMint( State storage state, @@ -432,24 +523,10 @@ library PoolLib { require(deadline == 0 || block.timestamp <= deadline, "swapMint: deadline"); require(state.lmsr.nAssets > 0, "swapMint: uninit pool"); - // Compute fee on gross maxAmountIn to get initial net estimate - (, uint256 netUintGuess) = _computeFee(maxAmountIn, swapFeePpm); - - // Convert the net guess to internal (floor) - int128 netInternalGuess = _uintToInternalFloor(netUintGuess, state.bases[inputTokenIndex]); - require(netInternalGuess > int128(0), "swapMint: input too small after fee"); - - // Use LMSR view to determine actual internal consumed and size-increase - (int128 amountInInternalUsed, int128 sizeIncreaseInternal) = state.lmsr.swapAmountsForMint(inputTokenIndex, netInternalGuess); - - // Convert to uint (ceil) to determine actual transfer - uint256 amountInUint = _internalToUintCeil(amountInInternalUsed, state.bases[inputTokenIndex]); - require(amountInUint > 0, "swapMint: input zero after internal conversion"); - - // Compute fee on actual used input and total transfer amount - uint256 feeUintActual = _ceilFee(amountInUint, swapFeePpm); - uint256 totalTransfer = amountInUint + feeUintActual; - require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMint: transfer exceeds max"); + // Calculate amounts using view function + (uint256 totalTransfer, uint256 amountInUint, uint256 feeUintActual, uint256 actualLpToMint) = swapMintAmounts( + state, inputTokenIndex, maxAmountIn, swapFeePpm, totalSupply + ); // Record pre-balance and transfer tokens uint256 prevBalI = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this)); @@ -460,30 +537,11 @@ library PoolLib { // Update cached uint balances state.cachedUintBalances[inputTokenIndex] = balIAfter; - // Compute old and new scaled size metrics - int128 oldTotal = _computeSizeMetric(state.lmsr.qInternal); - require(oldTotal > int128(0), "swapMint: zero total"); - uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE); - - int128 newTotal = oldTotal.add(sizeIncreaseInternal); - uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE); - - uint256 actualLpToMint; - if (totalSupply == 0) { - actualLpToMint = newScaled; - } else { - require(oldScaled > 0, "swapMint: oldScaled zero"); - uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0; - if (delta > 0) { - actualLpToMint = (totalSupply * delta) / oldScaled; - } else { - actualLpToMint = 0; - } - } - - require(actualLpToMint > 0, "swapMint: zero LP minted"); - // Update LMSR internal state + int128 oldTotal = _computeSizeMetric(state.lmsr.qInternal); + (, int128 sizeIncreaseInternal) = state.lmsr.swapAmountsForMint(inputTokenIndex, _uintToInternalFloor(amountInUint, state.bases[inputTokenIndex])); + int128 newTotal = oldTotal.add(sizeIncreaseInternal); + int128[] memory newQInternal = new int128[](n); for (uint256 idx = 0; idx < n; idx++) { newQInternal[idx] = state.lmsr.qInternal[idx].mul(newTotal).div(oldTotal); @@ -516,22 +574,8 @@ library PoolLib { require(totalSupply > 0, "burnSwap: empty supply"); require(payerBalance >= lpAmount, "burnSwap: insufficient LP"); - // alpha = lpAmount / supply as Q64.64 - int128 alpha = ABDKMath64x64.divu(lpAmount, totalSupply); - - // Use LMSR view to compute single-asset payout - (int128 payoutInternal, ) = state.lmsr.swapAmountsForBurn(inputTokenIndex, alpha); - - // Convert payoutInternal -> uint (floor) to favor pool - amountOutUint = _internalToUintFloor(payoutInternal, state.bases[inputTokenIndex]); - require(amountOutUint > 0, "burnSwap: output zero"); - - // Apply swap fee to the output - if (swapFeePpm > 0) { - uint256 feeUint = _ceilFee(amountOutUint, swapFeePpm); - require(amountOutUint > feeUint, "burnSwap: fee exceeds output"); - amountOutUint -= feeUint; - } + // Calculate amounts using view function + amountOutUint = burnSwapAmounts(state, lpAmount, inputTokenIndex, swapFeePpm, totalSupply); // Transfer the payout to receiver state.tokens[inputTokenIndex].safeTransfer(receiver, amountOutUint);