// 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"; /// @title PartyPoolSwapMintImpl - Implementation contract for swapMint and burnSwap functions /// @notice This contract contains the swapMint and burnSwap implementation that will be called via delegatecall /// @dev This contract inherits from PartyPoolBase to access storage and internal functions contract PartyPoolSwapMintImpl is PartyPoolBase { using ABDKMath64x64 for int128; 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); constructor() PartyPoolBase('','') {} /// @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 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"); // Update cached uint balances for token inputTokenIndex (only inputTokenIndex changed externally) 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 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 ) 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(balanceOf(payer) >= lpAmount, "burnSwap: insufficient LP"); // alpha = lpAmount / supply as Q64.64 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"); // Transfer the payout to receiver tokens[inputTokenIndex].safeTransfer(receiver, amountOutUint); // Burn LP tokens from payer (authorization via allowance) if (msg.sender != payer) { uint256 allowed = allowance(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; 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; } /// @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; } }