From b126c52c7cde03fa1797d895f6db1535b0dbaaa5 Mon Sep 17 00:00:00 2001 From: tim Date: Fri, 3 Oct 2025 13:42:28 -0400 Subject: [PATCH] swapMintImpl moved into mintImpl --- src/PartyPool.sol | 60 ++---- src/PartyPoolBase.sol | 8 + src/PartyPoolMintImpl.sol | 378 +++++++++++++++++++++++++++++++++- src/PartyPoolSwapMintImpl.sol | 337 +----------------------------- 4 files changed, 393 insertions(+), 390 deletions(-) diff --git a/src/PartyPool.sol b/src/PartyPool.sol index 5927d0c..d965d78 100644 --- a/src/PartyPool.sol +++ b/src/PartyPool.sol @@ -15,6 +15,7 @@ import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol"; import {PartyPoolSwapMintImpl} from "./PartyPoolSwapMintImpl.sol"; import {ERC20External} from "./ERC20External.sol"; + /// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token /// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model. /// The pool issues an ERC20 LP token representing proportional ownership. @@ -250,13 +251,6 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { } } - /// @dev Helper to record cached balances as effectiveBalance = onchain - owed. Reverts if owed > onchain. - function _recordCachedBalance(uint256 idx, uint256 onchainBal) internal { - uint256 owed = protocolFeesOwed[idx]; - require(onchainBal >= owed, "balance < protocol owed"); - cachedUintBalances[idx] = onchainBal - owed; - } - /// @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. @@ -499,7 +493,7 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { function swapMintAmounts(uint256 inputTokenIndex, uint256 maxAmountIn) external view returns (uint256 amountInUsed, uint256 fee, uint256 lpMinted) { - return SWAP_MINT_IMPL.swapMintAmounts( + return MINT_IMPL.swapMintAmounts( inputTokenIndex, maxAmountIn, SWAP_FEE_PPM, @@ -511,7 +505,7 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { function burnSwapAmounts(uint256 lpAmount, uint256 inputTokenIndex) external view returns (uint256 amountOut) { - return SWAP_MINT_IMPL.burnSwapAmounts( + return MINT_IMPL.burnSwapAmounts( lpAmount, inputTokenIndex, SWAP_FEE_PPM, @@ -537,32 +531,18 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { uint256 deadline ) external returns (uint256 lpMinted) { bytes memory data = abi.encodeWithSignature( - "swapMint(address,address,uint256,uint256,uint256,uint256)", + "swapMint(address,address,uint256,uint256,uint256,uint256,uint256)", payer, receiver, inputTokenIndex, maxAmountIn, deadline, - SWAP_FEE_PPM + SWAP_FEE_PPM, + PROTOCOL_FEE_PPM ); - bytes memory result = Address.functionDelegateCall(address(SWAP_MINT_IMPL), data); - // New ABI: implementation returns (uint256 lpMinted, uint256 feeUintActual) - (uint256 lpOut, uint256 feeUintActual) = abi.decode(result, (uint256, uint256)); - - // Accrue protocol share (floor) from the fee on the input token - if (PROTOCOL_FEE_PPM > 0 && feeUintActual > 0 && PROTOCOL_FEE_ADDRESS != address(0)) { - uint256 protoShare = (feeUintActual * PROTOCOL_FEE_PPM) / 1_000_000; - if (protoShare > 0) { - protocolFeesOwed[inputTokenIndex] += protoShare; - } - } - - // Update cached balance for the input token to effective onchain - owed - uint256 bal = IERC20(tokens[inputTokenIndex]).balanceOf(address(this)); - _recordCachedBalance(inputTokenIndex, bal); - - return lpOut; + bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data); + return abi.decode(result, (uint256)); } /// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver. @@ -581,32 +561,18 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { uint256 deadline ) external returns (uint256 amountOutUint) { bytes memory data = abi.encodeWithSignature( - "burnSwap(address,address,uint256,uint256,uint256,uint256)", + "burnSwap(address,address,uint256,uint256,uint256,uint256,uint256)", payer, receiver, lpAmount, inputTokenIndex, deadline, - SWAP_FEE_PPM + SWAP_FEE_PPM, + PROTOCOL_FEE_PPM ); - bytes memory result = Address.functionDelegateCall(address(SWAP_MINT_IMPL), data); - // New ABI: implementation returns (uint256 amountOutUint, uint256 feeTokenUint) - (uint256 outAmt, uint256 feeTokenUint) = abi.decode(result, (uint256, uint256)); - - // Accrue protocol share (floor) from the token-side fee computed by implementation - if (PROTOCOL_FEE_PPM > 0 && feeTokenUint > 0 && PROTOCOL_FEE_ADDRESS != address(0)) { - uint256 protoShare = (feeTokenUint * PROTOCOL_FEE_PPM) / 1_000_000; - if (protoShare > 0) { - protocolFeesOwed[inputTokenIndex] += protoShare; - } - } - - // Update cached balance for the target token to effective onchain - owed - uint256 bal = IERC20(tokens[inputTokenIndex]).balanceOf(address(this)); - _recordCachedBalance(inputTokenIndex, bal); - - return outAmt; + bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data); + return abi.decode(result, (uint256)); } diff --git a/src/PartyPoolBase.sol b/src/PartyPoolBase.sol index 4015558..9bcad97 100644 --- a/src/PartyPoolBase.sol +++ b/src/PartyPoolBase.sol @@ -116,4 +116,12 @@ abstract contract PartyPoolBase is ERC20Internal, ReentrancyGuard { } return total; } + + /// @dev Helper to record cached balances as effectiveBalance = onchain - owed. Reverts if owed > onchain. + function _recordCachedBalance(uint256 idx, uint256 onchainBal) internal { + uint256 owed = protocolFeesOwed[idx]; + require(onchainBal >= owed, "balance < protocol owed"); + cachedUintBalances[idx] = onchainBal - owed; + } + } diff --git a/src/PartyPoolMintImpl.sol b/src/PartyPoolMintImpl.sol index c13fbd7..ff5fcf9 100644 --- a/src/PartyPoolMintImpl.sol +++ b/src/PartyPoolMintImpl.sol @@ -1,12 +1,13 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.30; -import "@abdk/ABDKMath64x64.sol"; -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"; +import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol"; +import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ERC20Internal} from "./ERC20Internal.sol"; +import {IPartyPool} from "./IPartyPool.sol"; +import {LMSRStabilized} from "./LMSRStabilized.sol"; +import {PartyPoolBase} from "./PartyPoolBase.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 @@ -20,6 +21,10 @@ contract PartyPoolMintImpl is PartyPoolBase { event Mint(address indexed payer, address indexed receiver, uint256[] depositAmounts, uint256 lpMinted); event Burn(address indexed payer, address indexed receiver, uint256[] withdrawAmounts, uint256 lpBurned); + // + // Initialization Mint + // + function initialMint(address receiver, uint256 lpTokens, int128 KAPPA) external returns (uint256 lpMinted) { uint256 n = tokens.length; @@ -55,6 +60,11 @@ contract PartyPoolMintImpl is PartyPoolBase { emit Mint(address(0), receiver, depositAmounts, lpMinted); } + + // + // Regular Mint and Burn + // + 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; @@ -192,7 +202,8 @@ contract PartyPoolMintImpl is PartyPoolBase { emit Burn(payer, receiver, withdrawAmounts, lpAmount); } - function mintAmounts(uint256 lpTokenAmount, uint256 numAssets, uint256 totalSupply, uint256[] memory cachedUintBalances) public pure + function mintAmounts(uint256 lpTokenAmount, + uint256 numAssets, uint256 totalSupply, uint256[] memory cachedUintBalances) public pure returns (uint256[] memory depositAmounts) { depositAmounts = new uint256[](numAssets); @@ -232,4 +243,357 @@ contract PartyPoolMintImpl is PartyPoolBase { return withdrawAmounts; } + + + // + // Swap-Mint and Burn-Swap + // + + /// @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. + /// @param payer who transfers the input token + /// @param receiver who receives the minted LP tokens + /// @param inputTokenIndex index of the input token + /// @param maxAmountIn maximum uint token input (inclusive of fee) + /// @param deadline optional deadline + /// @param swapFeePpm fee in parts-per-million for this pool + /// @return lpMinted actual LP minted (uint) + function swapMint( + address payer, + address receiver, + uint256 inputTokenIndex, + uint256 maxAmountIn, + uint256 deadline, + uint256 swapFeePpm, + uint256 protocolFeePpm + ) external returns (uint256 lpMinted) { + uint256 n = tokens.length; + require(inputTokenIndex < n, "swapMint: idx"); + require(maxAmountIn > 0, "swapMint: input zero"); + require(deadline == 0 || block.timestamp <= deadline, "swapMint: deadline"); + require(lmsr.nAssets > 0, "swapMint: uninit pool"); + + // compute fee on gross maxAmountIn to get an initial net estimate (we'll recompute based on actual used) + (, uint256 netUintGuess) = _computeFee(maxAmountIn, swapFeePpm); + + // Convert the net guess to internal (floor) + int128 netInternalGuess = _uintToInternalFloor(netUintGuess, bases[inputTokenIndex]); + require(netInternalGuess > int128(0), "swapMint: input too small after fee"); + + // Use LMSR view to determine actual internal consumed and size-increase (ΔS) for mint + (int128 amountInInternalUsed, int128 sizeIncreaseInternal) = lmsr.swapAmountsForMint(inputTokenIndex, netInternalGuess); + + // amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer + uint256 amountInUint = _internalToUintCeil(amountInInternalUsed, bases[inputTokenIndex]); + require(amountInUint > 0, "swapMint: input zero after internal conversion"); + + // Compute fee on the actual used input and total transfer amount (ceiling) + uint256 feeUintActual = _ceilFee(amountInUint, swapFeePpm); + uint256 totalTransfer = amountInUint + feeUintActual; + require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMint: transfer exceeds max"); + + // Record pre-balance and transfer tokens from payer, require exact receipt (revert on fee-on-transfer) + uint256 prevBalI = IERC20(tokens[inputTokenIndex]).balanceOf(address(this)); + tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransfer); + uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this)); + require(balIAfter == prevBalI + totalTransfer, "swapMint: non-standard tokenIn"); + + // Accrue protocol share (floor) from the fee on the input token + if (protocolFeePpm > 0 && feeUintActual > 0) { + uint256 protoShare = (feeUintActual * protocolFeePpm) / 1_000_000; + if (protoShare > 0) { + protocolFeesOwed[inputTokenIndex] += protoShare; + } + } + // Update cached balance for the input token to effective onchain - owed + _recordCachedBalance(inputTokenIndex, balIAfter); + + // Compute old and new scaled size metrics to determine LP minted + int128 oldTotal = _computeSizeMetric(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; + // Use natural ERC20 function since base contract inherits from ERC20 + uint256 currentSupply = _totalSupply; + if (currentSupply == 0) { + // If somehow supply zero (shouldn't happen as lmsr.nAssets>0), mint newScaled + actualLpToMint = newScaled; + } else { + require(oldScaled > 0, "swapMint: oldScaled zero"); + uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0; + if (delta > 0) { + // floor truncation rounds in favor of pool + actualLpToMint = (currentSupply * delta) / oldScaled; + } else { + actualLpToMint = 0; + } + } + + require(actualLpToMint > 0, "swapMint: zero LP minted"); + + // Update LMSR internal state: scale qInternal proportionally by newTotal/oldTotal + int128[] memory newQInternal = new int128[](n); + for (uint256 idx = 0; idx < n; idx++) { + // newQInternal[idx] = qInternal[idx] * (newTotal / oldTotal) + newQInternal[idx] = lmsr.qInternal[idx].mul(newTotal).div(oldTotal); + } + + // Update cached internal and kappa via updateForProportionalChange + lmsr.updateForProportionalChange(newQInternal); + + // Use natural ERC20 function since base contract inherits from ERC20 + _mint(receiver, actualLpToMint); + + // Emit SwapMint event with gross transfer, net input and fee (planned exact-in) + emit IPartyPool.SwapMint(payer, receiver, inputTokenIndex, totalTransfer, amountInUint, feeUintActual); + + // Emit standard Mint event which records deposit amounts and LP minted + emit IPartyPool.Mint(payer, receiver, new uint256[](n), actualLpToMint); + // Note: depositAmounts array omitted (empty) since swapMint uses single-token input + + 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 + /// @param receiver who receives the single asset + /// @param lpAmount amount of LP tokens to burn + /// @param inputTokenIndex index of target asset to receive + /// @param deadline optional deadline + /// @param swapFeePpm fee in parts-per-million for this pool (may be used for future fee logic) + /// @return amountOutUint uint amount of asset i sent to receiver + function burnSwap( + address payer, + address receiver, + uint256 lpAmount, + uint256 inputTokenIndex, + uint256 deadline, + uint256 swapFeePpm, + uint256 protocolFeePpm + ) external returns (uint256 amountOutUint) { + uint256 n = tokens.length; + require(inputTokenIndex < n, "burnSwap: idx"); + require(lpAmount > 0, "burnSwap: zero lp"); + require(deadline == 0 || block.timestamp <= deadline, "burnSwap: deadline"); + + uint256 supply = _totalSupply; + require(supply > 0, "burnSwap: empty supply"); + require(_balances[payer] >= lpAmount, "burnSwap: insufficient LP"); + + // alpha = lpAmount / supply as Q64.64 (adjusted for fee) + int128 alpha = ABDKMath64x64.divu(lpAmount, supply) // 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, ) = lmsr.swapAmountsForBurn(inputTokenIndex, alpha); + + // Convert payoutInternal -> uint (floor) to favor pool + amountOutUint = _internalToUintFloor(payoutInternal, bases[inputTokenIndex]); + require(amountOutUint > 0, "burnSwap: output zero"); + + // Compute gross payout (no swap fee) so we can determine token-side fee = gross - net + int128 alphaGross = ABDKMath64x64.divu(lpAmount, supply); // gross fraction (no swap fee) + (int128 payoutGrossInternal, ) = lmsr.swapAmountsForBurn(inputTokenIndex, alphaGross); + uint256 payoutGrossUint = _internalToUintFloor(payoutGrossInternal, bases[inputTokenIndex]); + uint256 feeTokenUint = (payoutGrossUint > amountOutUint) ? (payoutGrossUint - amountOutUint) : 0; + + // Accrue protocol share (floor) from the token-side fee + if (protocolFeePpm > 0 && feeTokenUint > 0) { + uint256 protoShare = (feeTokenUint * protocolFeePpm) / 1_000_000; + if (protoShare > 0) { + protocolFeesOwed[inputTokenIndex] += protoShare; + } + } + + // Transfer the payout to receiver + tokens[inputTokenIndex].safeTransfer(receiver, amountOutUint); + + // Burn LP tokens from payer (authorization via allowance) + if (msg.sender != payer) { + uint256 allowed = _allowances[payer][msg.sender]; + require(allowed >= lpAmount, "burnSwap: allowance insufficient"); + _approve(payer, msg.sender, allowed - lpAmount); + } + _burn(payer, lpAmount); + + // Update cached balances by reading on-chain balances for all tokens + int128[] memory newQInternal = new int128[](n); + for (uint256 idx = 0; idx < n; idx++) { + uint256 bal = IERC20(tokens[idx]).balanceOf(address(this)); + cachedUintBalances[idx] = bal; + _recordCachedBalance(inputTokenIndex, bal); + newQInternal[idx] = _uintToInternalFloor(bal, bases[idx]); + } + + // Emit BurnSwap with public-facing info only (do not expose ΔS or LP burned) + emit IPartyPool.BurnSwap(payer, receiver, inputTokenIndex, amountOutUint); + + // If entire pool drained, deinit; else update proportionally + bool allZero = true; + for (uint256 idx = 0; idx < n; idx++) { + if (newQInternal[idx] != int128(0)) { allZero = false; break; } + } + if (allZero) { + lmsr.deinit(); + } else { + lmsr.updateForProportionalChange(newQInternal); + } + + emit IPartyPool.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; + } + } diff --git a/src/PartyPoolSwapMintImpl.sol b/src/PartyPoolSwapMintImpl.sol index b821d98..c16adab 100644 --- a/src/PartyPoolSwapMintImpl.sol +++ b/src/PartyPoolSwapMintImpl.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 {IPartyPool} from "./IPartyPool.sol"; /// @title PartyPoolSwapMintImpl - Implementation contract for swapMint and burnSwap functions /// @notice This contract contains the swapMint and burnSwap implementation that will be called via delegatecall @@ -15,341 +16,5 @@ contract PartyPoolSwapMintImpl is PartyPoolBase { using LMSRStabilized for LMSRStabilized.State; using SafeERC20 for IERC20; - // Events that mirror the main contract events - event SwapMint(address indexed payer, address indexed receiver, uint256 indexed inputTokenIndex, uint256 totalTransfer, uint256 amountInUint, uint256 feeUintActual); - event BurnSwap(address indexed payer, address indexed receiver, uint256 indexed inputTokenIndex, uint256 amountOutUint); - event Mint(address indexed payer, address indexed receiver, uint256[] depositAmounts, uint256 lpMinted); - event Burn(address indexed payer, address indexed receiver, uint256[] withdrawAmounts, uint256 lpBurned); - - /// @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. - /// @param payer who transfers the input token - /// @param receiver who receives the minted LP tokens - /// @param inputTokenIndex index of the input token - /// @param maxAmountIn maximum uint token input (inclusive of fee) - /// @param deadline optional deadline - /// @param swapFeePpm fee in parts-per-million for this pool - /// @return lpMinted actual LP minted (uint) - function swapMint( - address payer, - address receiver, - uint256 inputTokenIndex, - uint256 maxAmountIn, - uint256 deadline, - uint256 swapFeePpm - ) external returns (uint256 lpMinted, uint256 feeUintActual) { - uint256 n = tokens.length; - require(inputTokenIndex < n, "swapMint: idx"); - require(maxAmountIn > 0, "swapMint: input zero"); - require(deadline == 0 || block.timestamp <= deadline, "swapMint: deadline"); - require(lmsr.nAssets > 0, "swapMint: uninit pool"); - - // compute fee on gross maxAmountIn to get an initial net estimate (we'll recompute based on actual used) - (, uint256 netUintGuess) = _computeFee(maxAmountIn, swapFeePpm); - - // Convert the net guess to internal (floor) - int128 netInternalGuess = _uintToInternalFloor(netUintGuess, bases[inputTokenIndex]); - require(netInternalGuess > int128(0), "swapMint: input too small after fee"); - - // Use LMSR view to determine actual internal consumed and size-increase (ΔS) for mint - (int128 amountInInternalUsed, int128 sizeIncreaseInternal) = lmsr.swapAmountsForMint(inputTokenIndex, netInternalGuess); - - // amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer - uint256 amountInUint = _internalToUintCeil(amountInInternalUsed, bases[inputTokenIndex]); - require(amountInUint > 0, "swapMint: input zero after internal conversion"); - - // Compute fee on the actual used input and total transfer amount (ceiling) - feeUintActual = _ceilFee(amountInUint, swapFeePpm); - uint256 totalTransfer = amountInUint + feeUintActual; - require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMint: transfer exceeds max"); - - // Record pre-balance and transfer tokens from payer, require exact receipt (revert on fee-on-transfer) - uint256 prevBalI = IERC20(tokens[inputTokenIndex]).balanceOf(address(this)); - tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransfer); - uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this)); - require(balIAfter == prevBalI + totalTransfer, "swapMint: non-standard tokenIn"); - - // Update cached uint balances for token inputTokenIndex (implementation writes onchain value; wrapper will set effective) - cachedUintBalances[inputTokenIndex] = balIAfter; - - // Compute old and new scaled size metrics to determine LP minted - int128 oldTotal = _computeSizeMetric(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; - // Use natural ERC20 function since base contract inherits from ERC20 - uint256 currentSupply = _totalSupply; - if (currentSupply == 0) { - // If somehow supply zero (shouldn't happen as lmsr.nAssets>0), mint newScaled - actualLpToMint = newScaled; - } else { - require(oldScaled > 0, "swapMint: oldScaled zero"); - uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0; - if (delta > 0) { - // floor truncation rounds in favor of pool - actualLpToMint = (currentSupply * delta) / oldScaled; - } else { - actualLpToMint = 0; - } - } - - require(actualLpToMint > 0, "swapMint: zero LP minted"); - - // Update LMSR internal state: scale qInternal proportionally by newTotal/oldTotal - int128[] memory newQInternal = new int128[](n); - for (uint256 idx = 0; idx < n; idx++) { - // newQInternal[idx] = qInternal[idx] * (newTotal / oldTotal) - newQInternal[idx] = lmsr.qInternal[idx].mul(newTotal).div(oldTotal); - } - - // Update cached internal and kappa via updateForProportionalChange - lmsr.updateForProportionalChange(newQInternal); - - // Use natural ERC20 function since base contract inherits from ERC20 - _mint(receiver, actualLpToMint); - - // Emit SwapMint event with gross transfer, net input and fee (planned exact-in) - emit SwapMint(payer, receiver, inputTokenIndex, totalTransfer, amountInUint, feeUintActual); - - // Emit standard Mint event which records deposit amounts and LP minted - emit Mint(payer, receiver, new uint256[](n), actualLpToMint); - // Note: depositAmounts array omitted (empty) since swapMint uses single-token input - - lpMinted = actualLpToMint; - return (lpMinted, feeUintActual); - } - - /// @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 - /// @param receiver who receives the single asset - /// @param lpAmount amount of LP tokens to burn - /// @param inputTokenIndex index of target asset to receive - /// @param deadline optional deadline - /// @param swapFeePpm fee in parts-per-million for this pool (may be used for future fee logic) - /// @return amountOutUint uint amount of asset i sent to receiver - function burnSwap( - address payer, - address receiver, - uint256 lpAmount, - uint256 inputTokenIndex, - uint256 deadline, - uint256 swapFeePpm - ) external returns (uint256 amountOutUint, uint256 feeTokenUint) { - uint256 n = tokens.length; - require(inputTokenIndex < n, "burnSwap: idx"); - require(lpAmount > 0, "burnSwap: zero lp"); - require(deadline == 0 || block.timestamp <= deadline, "burnSwap: deadline"); - - uint256 supply = _totalSupply; - require(supply > 0, "burnSwap: empty supply"); - require(_balances[payer] >= lpAmount, "burnSwap: insufficient LP"); - - // alpha = lpAmount / supply as Q64.64 (adjusted for fee) - int128 alpha = ABDKMath64x64.divu(lpAmount, supply) // 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, ) = lmsr.swapAmountsForBurn(inputTokenIndex, alpha); - - // Convert payoutInternal -> uint (floor) to favor pool - amountOutUint = _internalToUintFloor(payoutInternal, bases[inputTokenIndex]); - require(amountOutUint > 0, "burnSwap: output zero"); - - // Compute gross payout (no swap fee) so we can determine token-side fee = gross - net - int128 alphaGross = ABDKMath64x64.divu(lpAmount, supply); // gross fraction (no swap fee) - (int128 payoutGrossInternal, ) = lmsr.swapAmountsForBurn(inputTokenIndex, alphaGross); - uint256 payoutGrossUint = _internalToUintFloor(payoutGrossInternal, bases[inputTokenIndex]); - feeTokenUint = (payoutGrossUint > amountOutUint) ? (payoutGrossUint - amountOutUint) : 0; - - // Transfer the payout to receiver - tokens[inputTokenIndex].safeTransfer(receiver, amountOutUint); - - // Burn LP tokens from payer (authorization via allowance) - if (msg.sender != payer) { - uint256 allowed = _allowances[payer][msg.sender]; - require(allowed >= lpAmount, "burnSwap: allowance insufficient"); - _approve(payer, msg.sender, allowed - lpAmount); - } - _burn(payer, lpAmount); - - // Update cached balances by reading on-chain balances for all tokens - int128[] memory newQInternal = new int128[](n); - for (uint256 idx = 0; idx < n; idx++) { - uint256 bal = IERC20(tokens[idx]).balanceOf(address(this)); - // implementation writes raw onchain values; wrapper will set effective cached values - cachedUintBalances[idx] = bal; - newQInternal[idx] = _uintToInternalFloor(bal, bases[idx]); - } - - // Emit BurnSwap with public-facing info only (do not expose ΔS or LP burned) - emit BurnSwap(payer, receiver, inputTokenIndex, amountOutUint); - - // If entire pool drained, deinit; else update proportionally - bool allZero = true; - for (uint256 idx = 0; idx < n; idx++) { - if (newQInternal[idx] != int128(0)) { allZero = false; break; } - } - if (allZero) { - lmsr.deinit(); - } else { - lmsr.updateForProportionalChange(newQInternal); - } - - emit Burn(payer, receiver, new uint256[](n), lpAmount); - return (amountOutUint, feeTokenUint); - } - - /// @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; - } }