From 8e69bfac5caec60a7c7797c83cf7d9715d523777 Mon Sep 17 00:00:00 2001 From: tim Date: Mon, 29 Sep 2025 17:07:55 -0400 Subject: [PATCH] PoolLib --- src/PartyPool.sol | 748 +++-------------------------------------- src/PoolLib.sol | 839 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 878 insertions(+), 709 deletions(-) create mode 100644 src/PoolLib.sol diff --git a/src/PartyPool.sol b/src/PartyPool.sol index fdbe468..aa2107f 100644 --- a/src/PartyPool.sol +++ b/src/PartyPool.sol @@ -1,15 +1,11 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.30; -import "@abdk/ABDKMath64x64.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; -import "./LMSRStabilized.sol"; -import "./LMSRStabilizedBalancedPair.sol"; +import "./PoolLib.sol"; import "./IPartyPool.sol"; -import "./IPartyFlashCallback.sol"; /// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token /// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model. @@ -20,36 +16,12 @@ import "./IPartyFlashCallback.sol"; /// - Exact-input swaps and swaps-to-price-limits, /// - Flash loans via a callback interface. /// -/// @dev The contract stores per-token uint "bases" used to scale token units into the internal Q64.64 -/// representation used by the LMSR library. Cached on-chain uint balances are kept to reduce balanceOf calls. -/// The contract uses ceiling/floor rules described in function comments to bias rounding in favor of the pool -/// (i.e., floor outputs to users, ceil inputs/fees where appropriate). +/// @dev The contract uses PoolLib for all implementation logic and maintains state in a PoolLib.State struct contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { - using ABDKMath64x64 for int128; - using LMSRStabilized for LMSRStabilized.State; - using SafeERC20 for IERC20; + using PoolLib for PoolLib.State; - - /// @notice Token addresses comprising the pool. Effectively immutable after construction. - /// @dev tokens[i] corresponds to the i-th asset and maps to index i in the internal LMSR arrays. - IERC20[] public tokens; // effectively immutable since there is no interface to change the tokens - - LMSRStabilized.State internal lmsr; - - // Cached on-chain balances (uint) and internal 64.64 representation - // balance / base = internal - uint256[] internal cachedUintBalances; - - /// @notice Per-token uint base denominators used to convert uint token amounts <-> internal Q64.64 representation. - /// @dev denominators()[i] is the base for tokens[i]. These bases are chosen by deployer and must match token decimals. - uint256[] internal bases; // per-token uint base used to scale token amounts <-> internal - - - /// @inheritdoc IPartyPool - function numTokens() external view returns (uint256) { return tokens.length; } - - /// @inheritdoc IPartyPool - function allTokens() external view returns (IERC20[] memory) { return tokens; } + /// @notice Pool state containing all storage variables + PoolLib.State internal s; /// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q) /// @dev Pool is constructed with a fixed κ. Clients may use LMSRStabilized.computeKappaFromSlippage(...) to @@ -67,11 +39,22 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { /// @inheritdoc IPartyPool - function denominators() external view returns (uint256[] memory) { return bases; } + function tokens(uint256 i) external view returns (IERC20) { return s.tokens[i]; } + + /// @inheritdoc IPartyPool + function numTokens() external view returns (uint256) { return s.tokens.length; } + + /// @inheritdoc IPartyPool + function allTokens() external view returns (IERC20[] memory) { return s.tokens; } + + /// @inheritdoc IPartyPool + function denominators() external view returns (uint256[] memory) { return s.bases; } /// @notice Mapping from token address => (index+1). A zero value indicates the token is not in the pool. /// @dev Use index = tokenAddressToIndexPlusOne[token] - 1 when non-zero. - mapping(IERC20=>uint) public tokenAddressToIndexPlusOne; // Uses index+1 so a result of 0 indicates a failed lookup + function tokenAddressToIndexPlusOne(IERC20 token) external view returns (uint256) { + return s.tokenAddressToIndexPlusOne[token]; + } /// @notice Scale factor used when converting LMSR Q64.64 totals to LP token units (uint). /// @dev LP tokens are minted in units equal to ABDK.mulu(lastTotalQ64x64, LP_SCALE). @@ -95,10 +78,6 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { uint256 flashFeePpm_, bool stable_ ) ERC20(name_, symbol_) { - require(tokens_.length > 1, "Pool: need >1 asset"); - require(tokens_.length == bases_.length, "Pool: lengths mismatch"); - tokens = tokens_; - bases = bases_; kappa = kappa_; require(swapFeePpm_ < 1_000_000, "Pool: fee >= ppm"); swapFeePpm = swapFeePpm_; @@ -106,50 +85,17 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { flashFeePpm = flashFeePpm_; _stablePair = stable_ && tokens_.length == 2; - uint256 n = tokens_.length; - - // Initialize LMSR state nAssets; full init occurs on first mint when quantities are known. - lmsr.nAssets = n; - - // Initialize token address to index mapping - for (uint i = 0; i < n;) { - tokenAddressToIndexPlusOne[tokens_[i]] = i + 1; - unchecked {i++;} - } - - // Initialize caches to zero - cachedUintBalances = new uint256[](n); + // Initialize state using library + s.initialize(tokens_, bases_); } - /* ---------------------- Initialization / Mint / Burn (LP token managed) ---------------------- */ /// @inheritdoc IPartyPool function mintDepositAmounts(uint256 lpTokenAmount) public view returns (uint256[] memory depositAmounts) { - uint256 n = tokens.length; - depositAmounts = new uint256[](n); - - // If this is the first mint or pool is empty, return zeros - // For first mint, tokens should already be transferred to the pool - if (totalSupply() == 0 || lmsr.nAssets == 0) { - return depositAmounts; // Return zeros, initial deposit handled differently - } - - // Calculate deposit based on current proportions - uint256 totalLpSupply = totalSupply(); - - // lpTokenAmount / totalLpSupply = depositAmount / currentBalance - // Therefore: depositAmount = (lpTokenAmount * currentBalance) / totalLpSupply - // We round up to protect the pool - for (uint i = 0; i < n; i++) { - uint256 currentBalance = cachedUintBalances[i]; - // Calculate with rounding up: (a * b + c - 1) / c - depositAmounts[i] = (lpTokenAmount * currentBalance + totalLpSupply - 1) / totalLpSupply; - } - - return depositAmounts; + return s.mintDepositAmounts(lpTokenAmount, totalSupply()); } /// @notice Initial mint to set up pool for the first time. @@ -159,37 +105,8 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { /// @param lpTokens The number of LP tokens to issue for this mint. If 0, then the number of tokens returned will equal the LMSR internal q total 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"); + lpMinted = s.initialMint(receiver, lpTokens, kappa, totalSupply()); _mint(receiver, lpMinted); - emit Mint(address(0), receiver, depositAmounts, lpMinted); } /// @notice Proportional mint for existing pool. @@ -202,94 +119,13 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { /// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore. function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external nonReentrant returns (uint256 lpMinted) { - require(deadline == 0 || block.timestamp <= deadline, "mint: deadline exceeded"); - uint256 n = tokens.length; - - // Check if this is NOT initial deposit - revert if it is - bool isInitialDeposit = totalSupply() == 0 || lmsr.nAssets == 0; - require(!isInitialDeposit, "mint: use initialMint for pool initialization"); - require(lpTokenAmount > 0, "mint: zero LP amount"); - - // Capture old pool size metric (scaled) by computing from current balances - int128 oldTotal = _computeSizeMetric(lmsr.qInternal); - uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE); - - // Calculate required deposit amounts for the desired LP tokens - uint256[] memory depositAmounts = mintDepositAmounts(lpTokenAmount); - - // Transfer in all token amounts - for (uint i = 0; i < n; ) { - if (depositAmounts[i] > 0) { - tokens[i].safeTransferFrom(payer, address(this), depositAmounts[i]); - } - unchecked { i++; } - } - - // Update cached balances for all assets - int128[] memory newQInternal = new int128[](n); - for (uint i = 0; i < n; ) { - uint256 bal = IERC20(tokens[i]).balanceOf(address(this)); - cachedUintBalances[i] = bal; - newQInternal[i] = _uintToInternalFloor(bal, bases[i]); - unchecked { i++; } - } - - // Update for proportional change - lmsr.updateForProportionalChange(newQInternal); - - // Compute actual LP tokens to mint based on change in size metric (scaled) - // floor truncation rounds in favor of the pool - int128 newTotal = _computeSizeMetric(newQInternal); - uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE); - uint256 actualLpToMint; - - require(oldScaled > 0, "mint: oldScaled zero"); - uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0; - // Proportional issuance: totalSupply * delta / oldScaled - if (delta > 0) { - // floor truncation rounds in favor of the pool - actualLpToMint = (totalSupply() * delta) / oldScaled; - } else { - actualLpToMint = 0; - } - - // Ensure the calculated LP amount is not too different from requested - require(actualLpToMint > 0, "mint: zero LP minted"); - - // Allow actual amount to be at most 0.00001% less than requested - // This accounts for rounding in deposit calculations - uint256 minAcceptable = lpTokenAmount * 99_999 / 100_000; - require(actualLpToMint >= minAcceptable, "mint: insufficient LP minted"); - - _mint(receiver, actualLpToMint); - emit Mint(payer, receiver, depositAmounts, actualLpToMint); - return actualLpToMint; + lpMinted = s.mint(payer, receiver, lpTokenAmount, deadline, totalSupply()); + _mint(receiver, lpMinted); } /// @inheritdoc IPartyPool function burnReceiveAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts) { - return _burnReceiveAmounts(lpTokenAmount); - } - - function _burnReceiveAmounts(uint256 lpTokenAmount) internal view returns (uint256[] memory withdrawAmounts) { - uint256 n = tokens.length; - withdrawAmounts = new uint256[](n); - - // If supply is zero or pool uninitialized, return zeros - if (totalSupply() == 0 || lmsr.nAssets == 0) { - return withdrawAmounts; // Return zeros, nothing to withdraw - } - - // Calculate withdrawal amounts based on current proportions - uint256 totalLpSupply = totalSupply(); - - // withdrawAmount = floor(lpTokenAmount * currentBalance / totalLpSupply) - for (uint i = 0; i < n; i++) { - uint256 currentBalance = cachedUintBalances[i]; - withdrawAmounts[i] = (lpTokenAmount * currentBalance) / totalLpSupply; - } - - return withdrawAmounts; + return s.burnReceiveAmounts(lpTokenAmount, totalSupply()); } /// @notice Burn LP tokens and withdraw the proportional basket to receiver. @@ -300,67 +136,15 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { /// @param lpAmount amount of LP tokens to burn (proportional withdrawal) /// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore. function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external nonReentrant { - require(deadline == 0 || block.timestamp <= deadline, "burn: deadline exceeded"); - uint256 n = tokens.length; - require(lpAmount > 0, "burn: zero lp"); + uint256[] memory withdrawAmounts = s.burn(payer, receiver, lpAmount, deadline, totalSupply(), balanceOf(payer)); - uint256 supply = totalSupply(); - require(supply > 0, "burn: empty supply"); - require(lmsr.nAssets > 0, "burn: uninit pool"); - require(balanceOf(payer) >= lpAmount, "burn: insufficient LP"); - - // Refresh cached balances to reflect current on-chain balances before computing withdrawal amounts - for (uint i = 0; i < n; ) { - uint256 bal = IERC20(tokens[i]).balanceOf(address(this)); - cachedUintBalances[i] = bal; - unchecked { i++; } - } - - // Compute proportional withdrawal amounts for the requested LP amount (rounded down) - uint256[] memory withdrawAmounts = _burnReceiveAmounts(lpAmount); - - // Transfer underlying tokens out to receiver according to computed proportions - for (uint i = 0; i < n; ) { - if (withdrawAmounts[i] > 0) { - tokens[i].safeTransfer(receiver, withdrawAmounts[i]); - } - unchecked { i++; } - } - - // Update cached balances and internal q for all assets - int128[] memory newQInternal = new int128[](n); - for (uint i = 0; i < n; ) { - uint256 bal = IERC20(tokens[i]).balanceOf(address(this)); - cachedUintBalances[i] = bal; - newQInternal[i] = _uintToInternalFloor(bal, bases[i]); - unchecked { i++; } - } - - // Apply proportional update or deinitialize if drained - bool allZero = true; - for (uint i = 0; i < n; ) { - if (newQInternal[i] != int128(0)) { - allZero = false; - break; - } - unchecked { i++; } - } - - if (allZero) { - lmsr.deinit(); - } else { - lmsr.updateForProportionalChange(newQInternal); - } - - // Burn exactly the requested LP amount from payer (authorization via allowance) + // Handle LP token burning with allowance if (msg.sender != payer) { uint256 allowed = allowance(payer, msg.sender); require(allowed >= lpAmount, "burn: allowance insufficient"); _approve(payer, msg.sender, allowed - lpAmount); } _burn(payer, lpAmount); - - emit Burn(payer, receiver, withdrawAmounts, lpAmount); } /* ---------------------- @@ -374,8 +158,7 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { uint256 maxAmountIn, int128 limitPrice ) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) { - (uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice); - return (grossIn, outUint, feeUint); + return s.swapAmounts(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm, _stablePair); } /// @inheritdoc IPartyPool @@ -384,11 +167,9 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { uint256 outputTokenIndex, int128 limitPrice ) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) { - (uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapToLimit(inputTokenIndex, outputTokenIndex, limitPrice); - return (grossIn, outUint, feeUint); + return s.swapToLimitAmounts(inputTokenIndex, outputTokenIndex, limitPrice, swapFeePpm); } - /// @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. @@ -409,39 +190,7 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { int128 limitPrice, uint256 deadline ) external nonReentrant returns (uint256 amountIn, uint256 amountOut, uint256 fee) { - uint256 n = tokens.length; - require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx"); - require(maxAmountIn > 0, "swap: input zero"); - require(deadline == 0 || block.timestamp <= deadline, "swap: deadline exceeded"); - - // Read previous balances for affected assets - uint256 prevBalI = IERC20(tokens[inputTokenIndex]).balanceOf(address(this)); - uint256 prevBalJ = IERC20(tokens[outputTokenIndex]).balanceOf(address(this)); - - // Compute amounts using the same path as views - (uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalUsed, int128 amountOutInternal, , uint256 feeUint) = - _quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice); - - // Transfer the exact amount from payer and require exact receipt (revert on fee-on-transfer) - tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount); - uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this)); - require(balIAfter == prevBalI + totalTransferAmount, "swap: non-standard tokenIn"); - - // Transfer output to receiver and verify exact decrease - tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint); - uint256 balJAfter = IERC20(tokens[outputTokenIndex]).balanceOf(address(this)); - require(balJAfter == prevBalJ - amountOutUint, "swap: non-standard tokenOut"); - - // Update cached uint balances for i and j using actual balances - cachedUintBalances[inputTokenIndex] = balIAfter; - cachedUintBalances[outputTokenIndex] = balJAfter; - - // Apply swap to LMSR state with the internal amounts actually used - lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalUsed, amountOutInternal); - - emit Swap(payer, receiver, tokens[inputTokenIndex], tokens[outputTokenIndex], totalTransferAmount, amountOutUint); - - return (totalTransferAmount, amountOutUint, feeUint); + return s.swap(payer, receiver, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, deadline, swapFeePpm, _stablePair); } /// @notice Swap up to the price limit; computes max input to reach limit then performs swap. @@ -456,175 +205,9 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { int128 limitPrice, uint256 deadline ) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) { - uint256 n = tokens.length; - require(inputTokenIndex < n && outputTokenIndex < n, "swapToLimit: idx"); - require(limitPrice > int128(0), "swapToLimit: limit <= 0"); - require(deadline == 0 || block.timestamp <= deadline, "swapToLimit: deadline exceeded"); - - // Read previous balances for affected assets - uint256 prevBalI = IERC20(tokens[inputTokenIndex]).balanceOf(address(this)); - uint256 prevBalJ = IERC20(tokens[outputTokenIndex]).balanceOf(address(this)); - - // Compute amounts using the same path as views - (uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalMax, int128 amountOutInternal, uint256 amountInUsedUint, uint256 feeUint) = - _quoteSwapToLimit(inputTokenIndex, outputTokenIndex, limitPrice); - - // Transfer the exact amount needed from payer and require exact receipt (revert on fee-on-transfer) - tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount); - uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this)); - require(balIAfter == prevBalI + totalTransferAmount, "swapToLimit: non-standard tokenIn"); - - // Transfer output to receiver and verify exact decrease - tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint); - uint256 balJAfter = IERC20(tokens[outputTokenIndex]).balanceOf(address(this)); - require(balJAfter == prevBalJ - amountOutUint, "swapToLimit: non-standard tokenOut"); - - // Update caches to actual balances - cachedUintBalances[inputTokenIndex] = balIAfter; - cachedUintBalances[outputTokenIndex] = balJAfter; - - // Apply swap to LMSR state with the internal amounts - lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalMax, amountOutInternal); - - // Maintain original event semantics (logs input without fee) - emit Swap(payer, receiver, tokens[inputTokenIndex], tokens[outputTokenIndex], amountInUsedUint, amountOutUint); - - return (amountInUsedUint, amountOutUint, feeUint); + return s.swapToLimit(payer, receiver, inputTokenIndex, outputTokenIndex, limitPrice, deadline, swapFeePpm); } - /// @notice Ceiling fee helper: computes ceil(x * feePpm / 1_000_000) - /// @dev Internal helper; public-facing functions use this to ensure fees round up in favor of pool. - function _ceilFee(uint256 x, uint256 feePpm) internal pure returns (uint256) { - if (feePpm == 0) return 0; - // ceil division: (num + denom - 1) / denom - return (x * feePpm + 1_000_000 - 1) / 1_000_000; - } - - /// @notice Internal quote for exact-input swap that mirrors swap() rounding and fee application - /// @dev Returns amounts consistent with swap() semantics: grossIn includes fees (ceil), amountOut is floored. - /// @return grossIn amount to transfer in (inclusive of fee), amountOutUint output amount (uint), - /// amountInInternalUsed and amountOutInternal (64.64), amountInUintNoFee input amount excluding fee (uint), - /// feeUint fee taken from the gross input (uint) - function _quoteSwapExactIn( - uint256 inputTokenIndex, - uint256 outputTokenIndex, - uint256 maxAmountIn, - int128 limitPrice - ) - internal - view - returns ( - uint256 grossIn, - uint256 amountOutUint, - int128 amountInInternalUsed, - int128 amountOutInternal, - uint256 amountInUintNoFee, - uint256 feeUint - ) - { - uint256 n = tokens.length; - require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx"); - require(maxAmountIn > 0, "swap: input zero"); - require(lmsr.nAssets > 0, "swap: empty pool"); - - // Estimate max net input (fee on gross rounded up, then subtract) - (, uint256 netUintForSwap) = _computeFee(maxAmountIn); - - // Convert to internal (floor) - int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, bases[inputTokenIndex]); - require(deltaInternalI > int128(0), "swap: input too small after fee"); - - // Compute internal amounts using LMSR (exact-input with price limit) - // if _stablePair is true, use the optimized path - (amountInInternalUsed, amountOutInternal) = - _stablePair ? LMSRStabilizedBalancedPair.swapAmountsForExactInput(lmsr, inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice) - : lmsr.swapAmountsForExactInput(inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice); - - // Convert actual used input internal -> uint (ceil) - amountInUintNoFee = _internalToUintCeil(amountInInternalUsed, bases[inputTokenIndex]); - require(amountInUintNoFee > 0, "swap: input zero"); - - // Compute gross transfer including fee on the used input (ceil) - feeUint = 0; - grossIn = amountInUintNoFee; - if (swapFeePpm > 0) { - feeUint = _ceilFee(amountInUintNoFee, swapFeePpm); - grossIn += feeUint; - } - - // Ensure within user max - require(grossIn <= maxAmountIn, "swap: transfer exceeds max"); - - // Compute output (floor) - amountOutUint = _internalToUintFloor(amountOutInternal, bases[outputTokenIndex]); - require(amountOutUint > 0, "swap: output zero"); - } - - /// @notice Internal quote for swap-to-limit that mirrors swapToLimit() rounding and fee application - /// @dev Computes the input required to reach limitPrice and the resulting output; all rounding matches swapToLimit. - /// @return grossIn amount to transfer in (inclusive of fee), amountOutUint output amount (uint), - /// amountInInternal and amountOutInternal (64.64), amountInUintNoFee input amount excluding fee (uint), - /// feeUint fee taken from the gross input (uint) - function _quoteSwapToLimit( - uint256 inputTokenIndex, - uint256 outputTokenIndex, - int128 limitPrice - ) - internal - view - returns ( - uint256 grossIn, - uint256 amountOutUint, - int128 amountInInternal, - int128 amountOutInternal, - uint256 amountInUintNoFee, - uint256 feeUint - ) - { - uint256 n = tokens.length; - require(inputTokenIndex < n && outputTokenIndex < n, "swapToLimit: idx"); - require(limitPrice > int128(0), "swapToLimit: limit <= 0"); - require(lmsr.nAssets > 0, "swapToLimit: pool uninitialized"); - - // Compute internal maxima at the price limit - (amountInInternal, amountOutInternal) = lmsr.swapAmountsForPriceLimit(inputTokenIndex, outputTokenIndex, limitPrice); - - // Convert input to uint (ceil) and output to uint (floor) - amountInUintNoFee = _internalToUintCeil(amountInInternal, bases[inputTokenIndex]); - require(amountInUintNoFee > 0, "swapToLimit: input zero"); - - feeUint = 0; - grossIn = amountInUintNoFee; - if (swapFeePpm > 0) { - feeUint = _ceilFee(amountInUintNoFee, swapFeePpm); - grossIn += feeUint; - } - - amountOutUint = _internalToUintFloor(amountOutInternal, bases[outputTokenIndex]); - require(amountOutUint > 0, "swapToLimit: output zero"); - } - - /// @notice Compute fee and net amounts for a gross input (fee rounded up to favor the pool). - /// @return feeUint fee taken (uint) and netUint remaining for protocol use (uint) - function _computeFee(uint256 gross) internal view returns (uint256 feeUint, uint256 netUint) { - if (swapFeePpm == 0) { - return (0, gross); - } - feeUint = _ceilFee(gross, swapFeePpm); - netUint = gross - feeUint; - } - - /// @notice Convenience: return gross = net + fee(net) using ceiling for fee. - function _addFee(uint256 netUint) internal view returns (uint256 gross) { - if (swapFeePpm == 0) return netUint; - uint256 fee = _ceilFee(netUint, swapFeePpm); - 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. - /// @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. @@ -641,90 +224,8 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { uint256 maxAmountIn, uint256 deadline ) external nonReentrant 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"); - - // Ensure pool initialized - 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); - - // 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; - if (totalSupply() == 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 = (totalSupply() * 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); - - // Note: we updated cachedUintBalances[inputTokenIndex] above via reading balance; other token uint balances did not - // change externally (they were not transferred in). We keep cachedUintBalances for others unchanged. - // Mint LP tokens to receiver - _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; + lpMinted = s.swapMint(payer, receiver, inputTokenIndex, maxAmountIn, deadline, swapFeePpm, totalSupply()); + _mint(receiver, lpMinted); } /// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver. @@ -742,76 +243,23 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { uint256 inputTokenIndex, uint256 deadline ) external nonReentrant 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"); + amountOutUint = s.burnSwap(payer, receiver, lpAmount, inputTokenIndex, deadline, swapFeePpm, totalSupply(), balanceOf(payer)); - 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); - - // 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) + // Handle LP token burning with 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; } - /// @inheritdoc IPartyPool function flashRepaymentAmounts(uint256[] memory loanAmounts) external view returns (uint256[] memory repaymentAmounts) { - repaymentAmounts = new uint256[](tokens.length); - for (uint256 i = 0; i < tokens.length; i++) { - uint256 amount = loanAmounts[i]; - if (amount > 0) { - repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm); - } - } + return s.flashRepaymentAmounts(loanAmounts, flashFeePpm); } - /// @notice Receive token amounts and require them to be repaid plus a fee inside a callback. /// @dev The caller must implement IPartyFlashCallback#partyFlashCallback which receives (amounts, repaymentAmounts, data). /// This function verifies that, after the callback returns, the pool's balances have increased by at least the fees @@ -824,137 +272,19 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { uint256[] memory amounts, bytes calldata data ) external nonReentrant { - require(recipient != address(0), "flash: zero recipient"); - require(amounts.length == tokens.length, "flash: amounts length mismatch"); - - // Calculate repayment amounts for each token including fee - uint256[] memory repaymentAmounts = new uint256[](tokens.length); - - // Store initial balances to verify repayment later - uint256[] memory initialBalances = new uint256[](tokens.length); - - // Track if any token amount is non-zero - bool hasNonZeroAmount = false; - - // Process each token, skipping those with zero amounts - for (uint256 i = 0; i < tokens.length; i++) { - uint256 amount = amounts[i]; - - if (amount > 0) { - hasNonZeroAmount = true; - - // Calculate repayment amount with fee (ceiling) - repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm); - - // Record initial balance - initialBalances[i] = IERC20(tokens[i]).balanceOf(address(this)); - - // Transfer token to recipient - tokens[i].safeTransfer(recipient, amount); - } - } - - // Ensure at least one token is being borrowed - require(hasNonZeroAmount, "flash: no tokens requested"); - - // Call flash callback with expected repayment amounts - IPartyFlashCallback(msg.sender).partyFlashCallback(amounts, repaymentAmounts, data); - - // Verify repayment amounts for tokens that were borrowed - for (uint256 i = 0; i < tokens.length; i++) { - if (amounts[i] > 0) { - uint256 currentBalance = IERC20(tokens[i]).balanceOf(address(this)); - - // Verify repayment: current balance must be at least (initial balance + fee) - require( - currentBalance >= initialBalances[i] + _ceilFee(amounts[i], flashFeePpm), - "flash: repayment failed" - ); - - // Update cached balance - cachedUintBalances[i] = currentBalance; - } - } - } - - - /* ---------------------- - Conversion helpers - ---------------------- */ - - // Convert uint token amount -> internal 64.64 (floor). Uses ABDKMath64x64.divu which truncates. - function _uintToInternalFloor(uint256 amount, uint256 base) internal pure returns (int128) { - // internal = amount / base (as Q64.64) - return ABDKMath64x64.divu(amount, base); - } - - // Convert internal 64.64 -> uint token amount (floor). Uses ABDKMath64x64.mulu which floors the product. - function _internalToUintFloor(int128 internalAmount, uint256 base) internal pure returns (uint256) { - // uint = internal * base (floored) - return ABDKMath64x64.mulu(internalAmount, base); - } - - // Convert internal 64.64 -> uint token amount (ceiling). Rounds up to protect the pool. - function _internalToUintCeil(int128 internalAmount, uint256 base) internal pure returns (uint256) { - // Get the floor value first - uint256 floorValue = ABDKMath64x64.mulu(internalAmount, base); - - // Check if there was any fractional part by comparing to a reconstruction of the original - int128 reconstructed = ABDKMath64x64.divu(floorValue, base); - - // If reconstructed is less than original, there was a fractional part that was truncated - if (reconstructed < internalAmount) { - return floorValue + 1; - } - - return floorValue; + s.flash(recipient, amounts, data, flashFeePpm); } /// @notice Marginal price of `base` in terms of `quote` (p_quote / p_base) as Q64.64 /// @dev Returns the LMSR marginal price directly (raw 64.64) for use by off-chain quoting logic. function price(uint256 baseTokenIndex, uint256 quoteTokenIndex) external view returns (int128) { - uint256 n = tokens.length; - require(baseTokenIndex < n && quoteTokenIndex < n, "price: idx"); - require(lmsr.nAssets > 0, "price: uninit"); - return lmsr.price(baseTokenIndex, quoteTokenIndex); + return s.price(baseTokenIndex, quoteTokenIndex); } /// @notice Price of one LP token denominated in `quote` asset as Q64.64 /// @dev Computes LMSR poolPrice (quote per unit qTotal) and scales it by totalSupply() / qTotal /// to return price per LP token unit in quote asset (raw 64.64). function poolPrice(uint256 quoteTokenIndex) external view returns (int128) { - uint256 n = tokens.length; - require(quoteTokenIndex < n, "poolPrice: idx"); - require(lmsr.nAssets > 0, "poolPrice: uninit"); - - // price per unit of qTotal (Q64.64) from LMSR - int128 pricePerQ = lmsr.poolPrice(quoteTokenIndex); - - // total internal q (qTotal) as Q64.64 - int128 qTotal = _computeSizeMetric(lmsr.qInternal); - require(qTotal > int128(0), "poolPrice: qTotal zero"); - - // totalSupply as Q64.64 - uint256 supply = totalSupply(); - require(supply > 0, "poolPrice: zero supply"); - int128 supplyQ64 = ABDKMath64x64.fromUInt(supply); - - // factor = totalSupply / qTotal (Q64.64) - int128 factor = supplyQ64.div(qTotal); - - // price per LP token = pricePerQ * factor (Q64.64) - return pricePerQ.mul(factor); + return s.poolPrice(quoteTokenIndex, totalSupply()); } - - /// @notice Helper to compute size metric (sum of all asset quantities) from internal balances - /// @dev Returns the sum of all provided qInternal_ entries as a Q64.64 value. - function _computeSizeMetric(int128[] memory qInternal_) private pure returns (int128) { - int128 total = int128(0); - for (uint i = 0; i < qInternal_.length; ) { - total = total.add(qInternal_[i]); - unchecked { i++; } - } - return total; - } - } diff --git a/src/PoolLib.sol b/src/PoolLib.sol new file mode 100644 index 0000000..cb257dd --- /dev/null +++ b/src/PoolLib.sol @@ -0,0 +1,839 @@ +// 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 "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "./LMSRStabilized.sol"; +import "./LMSRStabilizedBalancedPair.sol"; +import "./IPartyFlashCallback.sol"; + +/// @title PoolLib - Library containing all PartyPool implementation logic +/// @notice This library contains the core implementation for LMSR-backed multi-asset pools +/// @dev All functions are internal and accept State as the first parameter +library PoolLib { + using ABDKMath64x64 for int128; + using LMSRStabilized for LMSRStabilized.State; + using SafeERC20 for IERC20; + + /// @notice State struct containing all storage variables from PartyPool + /// @dev This struct is passed to all library functions as the first parameter + struct State { + /// @notice Token addresses comprising the pool + IERC20[] tokens; + + /// @notice LMSR state for pricing computations + LMSRStabilized.State lmsr; + + /// @notice Cached on-chain balances (uint) for each token + uint256[] cachedUintBalances; + + /// @notice Per-token uint base denominators used to convert uint <-> internal Q64.64 + uint256[] bases; + + /// @notice Mapping from token address => (index+1). Zero means token not in pool. + mapping(IERC20=>uint) tokenAddressToIndexPlusOne; + } + + /// @notice Scale factor used when converting LMSR Q64.64 totals to LP token units (uint) + uint256 internal constant LP_SCALE = 1e18; + + // Events that mirror the main contract events + event Mint(address indexed payer, address indexed receiver, uint256[] depositAmounts, uint256 lpMinted); + event Burn(address indexed payer, address indexed receiver, uint256[] withdrawAmounts, uint256 lpBurned); + event Swap(address indexed payer, address indexed receiver, IERC20 indexed tokenIn, IERC20 tokenOut, uint256 amountIn, uint256 amountOut); + 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); + + /// @notice Initialize the pool state with tokens and bases + /// @param state The pool state + /// @param tokens_ Array of token addresses + /// @param bases_ Array of base denominators for each token + function initialize( + State storage state, + IERC20[] memory tokens_, + uint256[] memory bases_ + ) internal { + require(tokens_.length > 1, "Pool: need >1 asset"); + require(tokens_.length == bases_.length, "Pool: lengths mismatch"); + + state.tokens = tokens_; + state.bases = bases_; + + uint256 n = tokens_.length; + + // Initialize LMSR state nAssets; full init occurs on first mint + state.lmsr.nAssets = n; + + // Initialize token address to index mapping + for (uint i = 0; i < n;) { + state.tokenAddressToIndexPlusOne[tokens_[i]] = i + 1; + unchecked {i++;} + } + + // Initialize caches to zero + state.cachedUintBalances = new uint256[](n); + } + + /// @notice Get deposit amounts needed for minting LP tokens + function mintDepositAmounts( + State storage state, + uint256 lpTokenAmount, + uint256 totalSupply + ) internal view returns (uint256[] memory depositAmounts) { + uint256 n = state.tokens.length; + depositAmounts = new uint256[](n); + + // If this is the first mint or pool is empty, return zeros + if (totalSupply == 0 || state.lmsr.nAssets == 0) { + return depositAmounts; + } + + // Calculate deposit based on current proportions + for (uint i = 0; i < n; i++) { + uint256 currentBalance = state.cachedUintBalances[i]; + // Calculate with rounding up: (a * b + c - 1) / c + depositAmounts[i] = (lpTokenAmount * currentBalance + totalSupply - 1) / totalSupply; + } + + return depositAmounts; + } + + /// @notice Initial mint to set up pool for the first time + function initialMint( + State storage state, + address receiver, + uint256 lpTokens, + int128 kappa, + uint256 totalSupply + ) internal returns (uint256 lpMinted) { + uint256 n = state.tokens.length; + + // Check if this is initial deposit + bool isInitialDeposit = totalSupply == 0 || state.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(state.tokens[i]).balanceOf(address(this)); + state.cachedUintBalances[i] = bal; + newQInternal[i] = _uintToInternalFloor(bal, state.bases[i]); + depositAmounts[i] = bal; + unchecked { i++; } + } + + // Initialize the stabilized LMSR state with provided kappa + state.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"); + emit Mint(address(0), receiver, depositAmounts, lpMinted); + } + + /// @notice Proportional mint for existing pool + function mint( + State storage state, + address payer, + address receiver, + uint256 lpTokenAmount, + uint256 deadline, + uint256 totalSupply + ) internal returns (uint256 lpMinted) { + require(deadline == 0 || block.timestamp <= deadline, "mint: deadline exceeded"); + uint256 n = state.tokens.length; + + // Check if this is NOT initial deposit + bool isInitialDeposit = totalSupply == 0 || state.lmsr.nAssets == 0; + require(!isInitialDeposit, "mint: use initialMint for pool initialization"); + require(lpTokenAmount > 0, "mint: zero LP amount"); + + // Capture old pool size metric (scaled) + int128 oldTotal = _computeSizeMetric(state.lmsr.qInternal); + uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE); + + // Calculate required deposit amounts + uint256[] memory depositAmounts = mintDepositAmounts(state, lpTokenAmount, totalSupply); + + // Transfer in all token amounts + for (uint i = 0; i < n; ) { + if (depositAmounts[i] > 0) { + state.tokens[i].safeTransferFrom(payer, address(this), depositAmounts[i]); + } + unchecked { i++; } + } + + // Update cached balances for all assets + int128[] memory newQInternal = new int128[](n); + for (uint i = 0; i < n; ) { + uint256 bal = IERC20(state.tokens[i]).balanceOf(address(this)); + state.cachedUintBalances[i] = bal; + newQInternal[i] = _uintToInternalFloor(bal, state.bases[i]); + unchecked { i++; } + } + + // Update for proportional change + state.lmsr.updateForProportionalChange(newQInternal); + + // Compute actual LP tokens to mint + int128 newTotal = _computeSizeMetric(newQInternal); + uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE); + uint256 actualLpToMint; + + require(oldScaled > 0, "mint: oldScaled zero"); + uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0; + if (delta > 0) { + actualLpToMint = (totalSupply * delta) / oldScaled; + } else { + actualLpToMint = 0; + } + + require(actualLpToMint > 0, "mint: zero LP minted"); + + // Allow actual amount to be at most 0.00001% less than requested + uint256 minAcceptable = lpTokenAmount * 99_999 / 100_000; + require(actualLpToMint >= minAcceptable, "mint: insufficient LP minted"); + + emit Mint(payer, receiver, depositAmounts, actualLpToMint); + return actualLpToMint; + } + + /// @notice Get withdrawal amounts for burning LP tokens + function burnReceiveAmounts( + State storage state, + uint256 lpTokenAmount, + uint256 totalSupply + ) internal view returns (uint256[] memory withdrawAmounts) { + uint256 n = state.tokens.length; + withdrawAmounts = new uint256[](n); + + if (totalSupply == 0 || state.lmsr.nAssets == 0) { + return withdrawAmounts; + } + + for (uint i = 0; i < n; i++) { + uint256 currentBalance = state.cachedUintBalances[i]; + withdrawAmounts[i] = (lpTokenAmount * currentBalance) / totalSupply; + } + + return withdrawAmounts; + } + + /// @notice Burn LP tokens and withdraw proportional basket + function burn( + State storage state, + address payer, + address receiver, + uint256 lpAmount, + uint256 deadline, + uint256 totalSupply, + uint256 payerBalance + ) internal returns (uint256[] memory withdrawAmounts) { + require(deadline == 0 || block.timestamp <= deadline, "burn: deadline exceeded"); + uint256 n = state.tokens.length; + require(lpAmount > 0, "burn: zero lp"); + require(totalSupply > 0, "burn: empty supply"); + require(state.lmsr.nAssets > 0, "burn: uninit pool"); + require(payerBalance >= lpAmount, "burn: insufficient LP"); + + // Refresh cached balances + for (uint i = 0; i < n; ) { + uint256 bal = IERC20(state.tokens[i]).balanceOf(address(this)); + state.cachedUintBalances[i] = bal; + unchecked { i++; } + } + + // Compute proportional withdrawal amounts + withdrawAmounts = burnReceiveAmounts(state, lpAmount, totalSupply); + + // Transfer underlying tokens out + for (uint i = 0; i < n; ) { + if (withdrawAmounts[i] > 0) { + state.tokens[i].safeTransfer(receiver, withdrawAmounts[i]); + } + unchecked { i++; } + } + + // Update cached balances and internal q + int128[] memory newQInternal = new int128[](n); + for (uint i = 0; i < n; ) { + uint256 bal = IERC20(state.tokens[i]).balanceOf(address(this)); + state.cachedUintBalances[i] = bal; + newQInternal[i] = _uintToInternalFloor(bal, state.bases[i]); + unchecked { i++; } + } + + // Apply proportional update or deinitialize if drained + bool allZero = true; + for (uint i = 0; i < n; ) { + if (newQInternal[i] != int128(0)) { + allZero = false; + break; + } + unchecked { i++; } + } + + if (allZero) { + state.lmsr.deinit(); + } else { + state.lmsr.updateForProportionalChange(newQInternal); + } + + emit Burn(payer, receiver, withdrawAmounts, lpAmount); + } + + /// @notice Get swap amounts for exact input swap + function swapAmounts( + State storage state, + uint256 inputTokenIndex, + uint256 outputTokenIndex, + uint256 maxAmountIn, + int128 limitPrice, + uint256 swapFeePpm, + bool stablePair + ) internal view returns (uint256 amountIn, uint256 amountOut, uint256 fee) { + (uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn( + state, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm, stablePair + ); + return (grossIn, outUint, feeUint); + } + + /// @notice Get swap amounts for swap to price limit + function swapToLimitAmounts( + State storage state, + uint256 inputTokenIndex, + uint256 outputTokenIndex, + int128 limitPrice, + uint256 swapFeePpm + ) internal view returns (uint256 amountIn, uint256 amountOut, uint256 fee) { + (uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapToLimit( + state, inputTokenIndex, outputTokenIndex, limitPrice, swapFeePpm + ); + return (grossIn, outUint, feeUint); + } + + /// @notice Execute exact input swap + function swap( + State storage state, + address payer, + address receiver, + uint256 inputTokenIndex, + uint256 outputTokenIndex, + uint256 maxAmountIn, + int128 limitPrice, + uint256 deadline, + uint256 swapFeePpm, + bool stablePair + ) internal returns (uint256 amountIn, uint256 amountOut, uint256 fee) { + uint256 n = state.tokens.length; + require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx"); + require(maxAmountIn > 0, "swap: input zero"); + require(deadline == 0 || block.timestamp <= deadline, "swap: deadline exceeded"); + + // Read previous balances + uint256 prevBalI = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this)); + uint256 prevBalJ = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this)); + + // Compute amounts + (uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalUsed, int128 amountOutInternal, , uint256 feeUint) = + _quoteSwapExactIn(state, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm, stablePair); + + // Transfer exact amount from payer + state.tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount); + uint256 balIAfter = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this)); + require(balIAfter == prevBalI + totalTransferAmount, "swap: non-standard tokenIn"); + + // Transfer output to receiver + state.tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint); + uint256 balJAfter = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this)); + require(balJAfter == prevBalJ - amountOutUint, "swap: non-standard tokenOut"); + + // Update cached balances + state.cachedUintBalances[inputTokenIndex] = balIAfter; + state.cachedUintBalances[outputTokenIndex] = balJAfter; + + // Apply swap to LMSR state + state.lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalUsed, amountOutInternal); + + emit Swap(payer, receiver, state.tokens[inputTokenIndex], state.tokens[outputTokenIndex], totalTransferAmount, amountOutUint); + + return (totalTransferAmount, amountOutUint, feeUint); + } + + /// @notice Execute swap to price limit + function swapToLimit( + State storage state, + address payer, + address receiver, + uint256 inputTokenIndex, + uint256 outputTokenIndex, + int128 limitPrice, + uint256 deadline, + uint256 swapFeePpm + ) internal returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) { + uint256 n = state.tokens.length; + require(inputTokenIndex < n && outputTokenIndex < n, "swapToLimit: idx"); + require(limitPrice > int128(0), "swapToLimit: limit <= 0"); + require(deadline == 0 || block.timestamp <= deadline, "swapToLimit: deadline exceeded"); + + // Read previous balances + uint256 prevBalI = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this)); + uint256 prevBalJ = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this)); + + // Compute amounts + (uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalMax, int128 amountOutInternal, uint256 amountInUsedUint, uint256 feeUint) = + _quoteSwapToLimit(state, inputTokenIndex, outputTokenIndex, limitPrice, swapFeePpm); + + // Transfer exact amount from payer + state.tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount); + uint256 balIAfter = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this)); + require(balIAfter == prevBalI + totalTransferAmount, "swapToLimit: non-standard tokenIn"); + + // Transfer output to receiver + state.tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint); + uint256 balJAfter = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this)); + require(balJAfter == prevBalJ - amountOutUint, "swapToLimit: non-standard tokenOut"); + + // Update caches + state.cachedUintBalances[inputTokenIndex] = balIAfter; + state.cachedUintBalances[outputTokenIndex] = balJAfter; + + // Apply swap to LMSR state + state.lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalMax, amountOutInternal); + + emit Swap(payer, receiver, state.tokens[inputTokenIndex], state.tokens[outputTokenIndex], amountInUsedUint, amountOutUint); + + return (amountInUsedUint, amountOutUint, feeUint); + } + + /// @notice Single-token mint (swapMint) + function swapMint( + State storage state, + address payer, + address receiver, + uint256 inputTokenIndex, + uint256 maxAmountIn, + uint256 deadline, + uint256 swapFeePpm, + uint256 totalSupply + ) internal returns (uint256 lpMinted) { + uint256 n = state.tokens.length; + require(inputTokenIndex < n, "swapMint: idx"); + require(maxAmountIn > 0, "swapMint: input zero"); + 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"); + + // Record pre-balance and transfer tokens + uint256 prevBalI = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this)); + state.tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransfer); + uint256 balIAfter = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this)); + require(balIAfter == prevBalI + totalTransfer, "swapMint: non-standard tokenIn"); + + // 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[] memory newQInternal = new int128[](n); + for (uint256 idx = 0; idx < n; idx++) { + newQInternal[idx] = state.lmsr.qInternal[idx].mul(newTotal).div(oldTotal); + } + + state.lmsr.updateForProportionalChange(newQInternal); + + emit SwapMint(payer, receiver, inputTokenIndex, totalTransfer, amountInUint, feeUintActual); + emit Mint(payer, receiver, new uint256[](n), actualLpToMint); + + return actualLpToMint; + } + + /// @notice Burn LP tokens and swap to single asset (burnSwap) + function burnSwap( + State storage state, + address payer, + address receiver, + uint256 lpAmount, + uint256 inputTokenIndex, + uint256 deadline, + uint256 swapFeePpm, + uint256 totalSupply, + uint256 payerBalance + ) internal returns (uint256 amountOutUint) { + uint256 n = state.tokens.length; + require(inputTokenIndex < n, "burnSwap: idx"); + require(lpAmount > 0, "burnSwap: zero lp"); + require(deadline == 0 || block.timestamp <= deadline, "burnSwap: deadline"); + 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; + } + + // Transfer the payout to receiver + state.tokens[inputTokenIndex].safeTransfer(receiver, amountOutUint); + + // 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(state.tokens[idx]).balanceOf(address(this)); + state.cachedUintBalances[idx] = bal; + newQInternal[idx] = _uintToInternalFloor(bal, state.bases[idx]); + } + + 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) { + state.lmsr.deinit(); + } else { + state.lmsr.updateForProportionalChange(newQInternal); + } + + emit Burn(payer, receiver, new uint256[](n), lpAmount); + return amountOutUint; + } + + /// @notice Calculate flash loan repayment amounts + function flashRepaymentAmounts( + State storage state, + uint256[] memory loanAmounts, + uint256 flashFeePpm + ) internal view returns (uint256[] memory repaymentAmounts) { + repaymentAmounts = new uint256[](state.tokens.length); + for (uint256 i = 0; i < state.tokens.length; i++) { + uint256 amount = loanAmounts[i]; + if (amount > 0) { + repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm); + } + } + } + + /// @notice Execute flash loan + function flash( + State storage state, + address recipient, + uint256[] memory amounts, + bytes calldata data, + uint256 flashFeePpm + ) internal { + require(recipient != address(0), "flash: zero recipient"); + require(amounts.length == state.tokens.length, "flash: amounts length mismatch"); + + // Calculate repayment amounts for each token including fee + uint256[] memory repaymentAmounts = new uint256[](state.tokens.length); + + // Store initial balances to verify repayment later + uint256[] memory initialBalances = new uint256[](state.tokens.length); + + // Track if any token amount is non-zero + bool hasNonZeroAmount = false; + + // Process each token, skipping those with zero amounts + for (uint256 i = 0; i < state.tokens.length; i++) { + uint256 amount = amounts[i]; + + if (amount > 0) { + hasNonZeroAmount = true; + + // Calculate repayment amount with fee (ceiling) + repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm); + + // Record initial balance + initialBalances[i] = IERC20(state.tokens[i]).balanceOf(address(this)); + + // Transfer token to recipient + state.tokens[i].safeTransfer(recipient, amount); + } + } + + // Ensure at least one token is being borrowed + require(hasNonZeroAmount, "flash: no tokens requested"); + + // Call flash callback with expected repayment amounts + IPartyFlashCallback(msg.sender).partyFlashCallback(amounts, repaymentAmounts, data); + + // Verify repayment amounts for tokens that were borrowed + for (uint256 i = 0; i < state.tokens.length; i++) { + if (amounts[i] > 0) { + uint256 currentBalance = IERC20(state.tokens[i]).balanceOf(address(this)); + + // Verify repayment: current balance must be at least (initial balance + fee) + require( + currentBalance >= initialBalances[i] + _ceilFee(amounts[i], flashFeePpm), + "flash: repayment failed" + ); + + // Update cached balance + state.cachedUintBalances[i] = currentBalance; + } + } + } + + /// @notice Get marginal price between two tokens + function price( + State storage state, + uint256 baseTokenIndex, + uint256 quoteTokenIndex + ) internal view returns (int128) { + uint256 n = state.tokens.length; + require(baseTokenIndex < n && quoteTokenIndex < n, "price: idx"); + require(state.lmsr.nAssets > 0, "price: uninit"); + return state.lmsr.price(baseTokenIndex, quoteTokenIndex); + } + + /// @notice Get price of one LP token in quote asset + function poolPrice( + State storage state, + uint256 quoteTokenIndex, + uint256 totalSupply + ) internal view returns (int128) { + uint256 n = state.tokens.length; + require(quoteTokenIndex < n, "poolPrice: idx"); + require(state.lmsr.nAssets > 0, "poolPrice: uninit"); + + // price per unit of qTotal (Q64.64) from LMSR + int128 pricePerQ = state.lmsr.poolPrice(quoteTokenIndex); + + // total internal q (qTotal) as Q64.64 + int128 qTotal = _computeSizeMetric(state.lmsr.qInternal); + require(qTotal > int128(0), "poolPrice: qTotal zero"); + + // totalSupply as Q64.64 + require(totalSupply > 0, "poolPrice: zero supply"); + int128 supplyQ64 = ABDKMath64x64.fromUInt(totalSupply); + + // factor = totalSupply / qTotal (Q64.64) + int128 factor = supplyQ64.div(qTotal); + + // price per LP token = pricePerQ * factor (Q64.64) + return pricePerQ.mul(factor); + } + + // Internal helper functions + + /// @notice Ceiling fee helper + function _ceilFee(uint256 x, uint256 feePpm) internal pure returns (uint256) { + if (feePpm == 0) return 0; + return (x * feePpm + 1_000_000 - 1) / 1_000_000; + } + + /// @notice Compute fee and net amounts for a gross input + function _computeFee(uint256 gross, uint256 swapFeePpm) internal pure returns (uint256 feeUint, uint256 netUint) { + if (swapFeePpm == 0) { + return (0, gross); + } + feeUint = _ceilFee(gross, swapFeePpm); + netUint = gross - feeUint; + } + + /// @notice Add fee to net amount + function _addFee(uint256 netUint, uint256 swapFeePpm) internal pure returns (uint256 gross) { + if (swapFeePpm == 0) return netUint; + uint256 fee = _ceilFee(netUint, swapFeePpm); + return netUint + fee; + } + + /// @notice Internal quote for exact-input swap + function _quoteSwapExactIn( + State storage state, + uint256 inputTokenIndex, + uint256 outputTokenIndex, + uint256 maxAmountIn, + int128 limitPrice, + uint256 swapFeePpm, + bool stablePair + ) + internal + view + returns ( + uint256 grossIn, + uint256 amountOutUint, + int128 amountInInternalUsed, + int128 amountOutInternal, + uint256 amountInUintNoFee, + uint256 feeUint + ) + { + uint256 n = state.tokens.length; + require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx"); + require(maxAmountIn > 0, "swap: input zero"); + require(state.lmsr.nAssets > 0, "swap: empty pool"); + + // Estimate max net input + (, uint256 netUintForSwap) = _computeFee(maxAmountIn, swapFeePpm); + + // Convert to internal (floor) + int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, state.bases[inputTokenIndex]); + require(deltaInternalI > int128(0), "swap: input too small after fee"); + + // Compute internal amounts using LMSR + (amountInInternalUsed, amountOutInternal) = + stablePair ? LMSRStabilizedBalancedPair.swapAmountsForExactInput(state.lmsr, inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice) + : state.lmsr.swapAmountsForExactInput(inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice); + + // Convert actual used input internal -> uint (ceil) + amountInUintNoFee = _internalToUintCeil(amountInInternalUsed, state.bases[inputTokenIndex]); + require(amountInUintNoFee > 0, "swap: input zero"); + + // Compute gross transfer including fee + feeUint = 0; + grossIn = amountInUintNoFee; + if (swapFeePpm > 0) { + feeUint = _ceilFee(amountInUintNoFee, swapFeePpm); + grossIn += feeUint; + } + + // Ensure within user max + require(grossIn <= maxAmountIn, "swap: transfer exceeds max"); + + // Compute output (floor) + amountOutUint = _internalToUintFloor(amountOutInternal, state.bases[outputTokenIndex]); + require(amountOutUint > 0, "swap: output zero"); + } + + /// @notice Internal quote for swap-to-limit + function _quoteSwapToLimit( + State storage state, + uint256 inputTokenIndex, + uint256 outputTokenIndex, + int128 limitPrice, + uint256 swapFeePpm + ) + internal + view + returns ( + uint256 grossIn, + uint256 amountOutUint, + int128 amountInInternal, + int128 amountOutInternal, + uint256 amountInUintNoFee, + uint256 feeUint + ) + { + uint256 n = state.tokens.length; + require(inputTokenIndex < n && outputTokenIndex < n, "swapToLimit: idx"); + require(limitPrice > int128(0), "swapToLimit: limit <= 0"); + require(state.lmsr.nAssets > 0, "swapToLimit: pool uninitialized"); + + // Compute internal maxima at the price limit + (amountInInternal, amountOutInternal) = state.lmsr.swapAmountsForPriceLimit(inputTokenIndex, outputTokenIndex, limitPrice); + + // Convert input to uint (ceil) and output to uint (floor) + amountInUintNoFee = _internalToUintCeil(amountInInternal, state.bases[inputTokenIndex]); + require(amountInUintNoFee > 0, "swapToLimit: input zero"); + + feeUint = 0; + grossIn = amountInUintNoFee; + if (swapFeePpm > 0) { + feeUint = _ceilFee(amountInUintNoFee, swapFeePpm); + grossIn += feeUint; + } + + amountOutUint = _internalToUintFloor(amountOutInternal, state.bases[outputTokenIndex]); + require(amountOutUint > 0, "swapToLimit: output zero"); + } + + // Convert uint token amount -> internal 64.64 (floor) + function _uintToInternalFloor(uint256 amount, uint256 base) internal pure returns (int128) { + return ABDKMath64x64.divu(amount, base); + } + + // Convert internal 64.64 -> uint token amount (floor) + function _internalToUintFloor(int128 internalAmount, uint256 base) internal pure returns (uint256) { + return ABDKMath64x64.mulu(internalAmount, base); + } + + // Convert internal 64.64 -> uint token amount (ceiling) + function _internalToUintCeil(int128 internalAmount, uint256 base) internal pure returns (uint256) { + // Get the floor value first + uint256 floorValue = ABDKMath64x64.mulu(internalAmount, base); + + // Check if there was any fractional part by comparing to a reconstruction + int128 reconstructed = ABDKMath64x64.divu(floorValue, base); + + // If reconstructed is less than original, there was a fractional part that was truncated + if (reconstructed < internalAmount) { + return floorValue + 1; + } + + return floorValue; + } + + /// @notice Helper to compute size metric (sum of all asset quantities) from internal balances + function _computeSizeMetric(int128[] memory qInternal_) internal pure returns (int128) { + int128 total = int128(0); + for (uint i = 0; i < qInternal_.length; ) { + total = total.add(qInternal_[i]); + unchecked { i++; } + } + return total; + } +}