diff --git a/src/IPartyPool.sol b/src/IPartyPool.sol index af568e1..f3e4199 100644 --- a/src/IPartyPool.sol +++ b/src/IPartyPool.sol @@ -4,9 +4,18 @@ pragma solidity ^0.8.30; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; /// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token -/// @notice Uses LMSRStabilized library; stores per-token uint bases to convert to/from 64.64 fixed point. -/// - Caches qInternal[] (int128 64.64) and cachedUintBalances[] to minimize balanceOf() calls. -/// - swap and swapToLimit mimic core lib; mint/burn call updateForProportionalChange() and manage LP tokens. +/// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model. +/// The pool issues an ERC20 LP token representing proportional ownership. +/// It supports: +/// - Proportional minting and burning of LP tokens, +/// - Single-token mint (swapMint) and single-asset withdrawal (burnSwap), +/// - 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). interface IPartyPool is IERC20Metadata { // All int128's are ABDKMath64x64 format @@ -47,25 +56,51 @@ interface IPartyPool is IERC20Metadata { // Immutable pool configuration (public getters) + /// @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. function tokens(uint256) external view returns (address); // get single token + + /// @notice Returns the number of tokens (n) in the pool. function numTokens() external view returns (uint256); + + /// @notice Returns the list of all token addresses in the pool (copy). function allTokens() external view returns (address[] memory); + + /// @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. function denominators() external view returns (uint256[] memory); + + /// @notice Trade fraction (Q64.64) representing a reference trade size as fraction of one asset's inventory. + /// @dev Used by the LMSR stabilization logic to compute target slippage. function tradeFrac() external view returns (int128); // ABDK 64x64 + + /// @notice Target slippage (Q64.64) applied for the reference trade size specified by tradeFrac. function targetSlippage() external view returns (int128); // ABDK 64x64 + + /// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations. function swapFeePpm() external view returns (uint256); + + /// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts. + function flashFeePpm() external view returns (uint256); + + /// @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. function tokenAddressToIndexPlusOne(address) external view returns (uint); // Initialization / Mint / Burn (LP token managed) /// @notice Calculate the proportional deposit amounts required for a given LP token amount + /// @dev Returns the minimum token amounts (rounded up) that must be supplied to receive lpTokenAmount + /// LP tokens at current pool proportions. If the pool is empty (initial deposit) returns zeros + /// because the initial deposit is handled by transferring tokens then calling mint(). /// @param lpTokenAmount The amount of LP tokens desired /// @return depositAmounts Array of token amounts to deposit (rounded up) function mintDepositAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory depositAmounts); /// @notice Proportional mint (or initial supply if first call). - /// For initial supply: assumes tokens have already been transferred to the pool - /// For subsequent mints: payer must approve tokens beforehand, receiver gets the LP tokens + /// @dev - For initial supply: assumes tokens have already been transferred to the pool prior to calling. + /// - For subsequent mints: payer must approve the required token amounts before calling. + /// Rounds follow the pool-favorable conventions documented in helpers (ceil inputs, floor outputs). /// @param payer address that provides the input tokens (ignored for initial deposit) /// @param receiver address that receives the LP tokens /// @param lpTokenAmount desired amount of LP tokens to mint (ignored for initial deposit) @@ -73,12 +108,15 @@ interface IPartyPool is IERC20Metadata { function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external; /// @notice Calculate the proportional withdrawal amounts for a given LP token amount + /// @dev Returns the maximum token amounts (rounded down) that will be withdrawn when burning lpTokenAmount. + /// If the pool is uninitialized or supply is zero, returns zeros. /// @param lpTokenAmount The amount of LP tokens to burn /// @return withdrawAmounts Array of token amounts to withdraw (rounded down) function burnReceiveAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts); /// @notice Burn LP tokens and withdraw the proportional basket to receiver. - /// Payer must own the LP tokens; withdraw amounts are computed from current proportions. + /// @dev Payer must own or approve the LP tokens being burned. The function updates LMSR state + /// proportionally to reflect the reduced pool size after the withdrawal. /// @param payer address that provides the LP tokens to burn /// @param receiver address that receives the withdrawn tokens /// @param lpAmount amount of LP tokens to burn (proportional withdrawal) @@ -89,73 +127,114 @@ interface IPartyPool is IERC20Metadata { // Swaps /// @notice External view to quote exact-in swap amounts (gross input incl. fee and output), matching swap() computations + /// @param inputTokenIndex index of input token + /// @param outputTokenIndex index of output token + /// @param maxAmountIn maximum gross input allowed (inclusive of fee) + /// @param limitPrice maximum acceptable marginal price (pass 0 to ignore) + /// @return amountIn gross input amount to transfer (includes fee), amountOut output amount user would receive, fee fee amount taken function swapAmounts( - uint256 i, - uint256 j, + uint256 inputTokenIndex, + uint256 outputTokenIndex, uint256 maxAmountIn, int128 limitPrice ) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee); + /// @notice Swap input token inputTokenIndex -> token outputTokenIndex. Payer must approve token inputTokenIndex. + /// @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. + /// @param payer address of the account that pays for the swap + /// @param receiver address that will receive the output tokens + /// @param inputTokenIndex index of input asset + /// @param outputTokenIndex index of output asset + /// @param maxAmountIn maximum amount of token inputTokenIndex (uint256) to transfer in (inclusive of fees) + /// @param limitPrice maximum acceptable marginal price (64.64 fixed point). Pass 0 to ignore. + /// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore. + /// @return amountIn actual input used (uint256), amountOut actual output sent (uint256), fee fee taken from the input (uint256) function swap( address payer, address receiver, - uint256 i, - uint256 j, + uint256 inputTokenIndex, + uint256 outputTokenIndex, uint256 maxAmountIn, int128 limitPrice, uint256 deadline ) external returns (uint256 amountIn, uint256 amountOut, uint256 fee); /// @notice External view to quote swap-to-limit amounts (gross input incl. fee and output), matching swapToLimit() computations + /// @param inputTokenIndex index of input token + /// @param outputTokenIndex index of output token + /// @param limitPrice target marginal price to reach (must be > 0) + /// @return amountIn gross input amount to transfer (includes fee), amountOut output amount user would receive, fee fee amount taken function swapToLimitAmounts( - uint256 i, - uint256 j, + uint256 inputTokenIndex, + uint256 outputTokenIndex, int128 limitPrice ) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee); + /// @notice Swap up to the price limit; computes max input to reach limit then performs swap. + /// @dev If balances prevent fully reaching the limit, the function caps and returns actuals. + /// The payer must transfer the exact gross input computed by the view. + /// @param payer address of the account that pays for the swap + /// @param receiver address that will receive the output tokens + /// @param inputTokenIndex index of input asset + /// @param outputTokenIndex index of output asset + /// @param limitPrice target marginal price to reach (must be > 0) + /// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore. + /// @return amountInUsed actual input used excluding fee (uint256), amountOut actual output sent (uint256), fee fee taken from the input (uint256) function swapToLimit( address payer, address receiver, - uint256 i, - uint256 j, + uint256 inputTokenIndex, + uint256 outputTokenIndex, int128 limitPrice, uint256 deadline ) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee); /// @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 i index of the input token + /// @param inputTokenIndex index of the input token /// @param maxAmountIn maximum uint token input (inclusive of fee) /// @param deadline optional deadline /// @return lpMinted actual LP minted (uint) function swapMint( address payer, address receiver, - uint256 i, + uint256 inputTokenIndex, uint256 maxAmountIn, uint256 deadline ) external returns (uint256 lpMinted); - /// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `i` and send to receiver. + /// @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 i index of target asset to receive + /// @param inputTokenIndex index of target asset to receive /// @param deadline optional deadline - /// @return amountOutUint uint amount of asset i sent to receiver + /// @return amountOutUint uint amount of asset inputTokenIndex sent to receiver function burnSwap( address payer, address receiver, uint256 lpAmount, - uint256 i, + uint256 inputTokenIndex, uint256 deadline ) external returns (uint256 amountOutUint); - /// @notice Receive token0 and/or token1 and pay it back, plus a fee, in the callback - /// @dev The caller of this method receives a callback in the form of IPartyFlashCallback#partyFlashCallback + /// @notice Compute repayment amounts (principal + flash fee) for a proposed flash loan. + /// @param loanAmounts array of per-token loan amounts; must match the pool's token ordering. + /// @return repaymentAmounts array where repaymentAmounts[i] = loanAmounts[i] + ceil(loanAmounts[i] * flashFeePpm) + function flashRepaymentAmounts(uint256[] memory loanAmounts) external view + returns (uint256[] memory repaymentAmounts); + + /// @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 + /// for each borrowed token. Reverts if repayment (including fee) did not occur. /// @param recipient The address which will receive the token amounts - /// @param amounts The amount of each token to send + /// @param amounts The amount of each token to send (array length must equal pool size) /// @param data Any data to be passed through to the callback function flash( address recipient, diff --git a/src/PartyPool.sol b/src/PartyPool.sol index e6416c9..8726e77 100644 --- a/src/PartyPool.sol +++ b/src/PartyPool.sol @@ -13,9 +13,18 @@ import "./IPartyPool.sol"; import "./IPartyFlashCallback.sol"; /// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token -/// @notice Uses LMSRStabilized library; stores per-token uint bases to convert to/from 64.64 fixed point. -/// - Caches qInternal[] (int128 64.64) and cachedUintBalances[] to minimize balanceOf() calls. -/// - swap and swapToLimit mimic core lib; mint/burn call updateForProportionalChange() and manage LP tokens. +/// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model. +/// The pool issues an ERC20 LP token representing proportional ownership. +/// It supports: +/// - Proportional minting and burning of LP tokens, +/// - Single-token mint (swapMint) and single-asset withdrawal (burnSwap), +/// - 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). contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { using ABDKMath64x64 for int128; using LMSRStabilized for LMSRStabilized.State; @@ -26,20 +35,31 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { // Immutable pool configuration // + /// @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. address[] public tokens; // effectively immutable since there is no interface to change the tokens + + /// @inheritdoc IPartyPool function numTokens() external view returns (uint256) { return tokens.length; } - function allTokens() external view returns (address[] memory) { return tokens; } + + /// @inheritdoc IPartyPool + function allTokens() external view returns (address[] memory) { return tokens; } // NOTE that the slippage target is only exactly achieved in completely balanced pools where all assets are // priced the same. This target is actually a minimum slippage that the pool imposes on traders, and the actual // slippage cost can be multiples bigger in practice due to pool inventory imbalances. + + /// @notice Trade fraction (Q64.64) representing a reference trade size as fraction of one asset's inventory. + /// @dev Used by the LMSR stabilization logic to compute target slippage. int128 public immutable tradeFrac; // slippage target trade size as a fraction of one asset's inventory + + /// @notice Target slippage (Q64.64) applied for the reference trade size specified by tradeFrac. int128 public immutable targetSlippage; // target slippage applied to that trade size - // fee in parts-per-million (ppm), taken from inputs before swaps + /// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations. uint256 public immutable swapFeePpm; - // flash loan fee in parts-per-million (ppm) + /// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts. uint256 public immutable flashFeePpm; // @@ -47,16 +67,27 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { // LMSRStabilized.State internal lmsr; + + /// @notice If true and there are exactly two assets, an optimized 2-asset stable-pair path is used for some computations. bool immutable private _stablePair; // if true, the optimized LMSRStabilizedBalancedPair optimization path is enabled // 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 denominators() external view returns (uint256[] memory) { return 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(address=>uint) public tokenAddressToIndexPlusOne; // Uses index+1 so a result of 0 indicates a failed lookup + /// @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). uint256 public constant LP_SCALE = 1e18; // Scale used to convert LMSR lastTotal (Q64.64) into LP token units (uint) /// @param name_ LP token name @@ -111,9 +142,7 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { Initialization / Mint / Burn (LP token managed) ---------------------- */ - /// @notice Calculate the proportional deposit amounts required for a given LP token amount - /// @param lpTokenAmount The amount of LP tokens desired - /// @return depositAmounts Array of token amounts to deposit (rounded up) + /// @inheritdoc IPartyPool function mintDepositAmounts(uint256 lpTokenAmount) public view returns (uint256[] memory depositAmounts) { uint256 n = tokens.length; depositAmounts = new uint256[](n); @@ -139,9 +168,7 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { return depositAmounts; } - /// @notice Calculate the proportional withdrawal amounts for a given LP token amount - /// @param lpTokenAmount The amount of LP tokens to burn - /// @return withdrawAmounts Array of token amounts to withdraw (rounded down) + /// @inheritdoc IPartyPool function burnReceiveAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts) { return _burnReceiveAmounts(lpTokenAmount); } @@ -168,8 +195,9 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { } /// @notice Proportional mint (or initial supply if first call). - /// For initial supply: assumes tokens have already been transferred to the pool - /// For subsequent mints: payer must approve tokens beforehand, receiver gets the LP tokens + /// @dev - For initial supply: assumes tokens have already been transferred to the pool prior to calling. + /// - For subsequent mints: payer must approve the required token amounts before calling. + /// Rounds follow the pool-favorable conventions documented in helpers (ceil inputs, floor outputs). /// @param payer address that provides the input tokens (ignored for initial deposit) /// @param receiver address that receives the LP tokens /// @param lpTokenAmount desired amount of LP tokens to mint (ignored for initial deposit) @@ -267,7 +295,8 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { } /// @notice Burn LP tokens and withdraw the proportional basket to receiver. - /// Payer must own the LP tokens; withdraw amounts are computed from current proportions. + /// @dev Payer must own or approve the LP tokens being burned. The function updates LMSR state + /// proportionally to reflect the reduced pool size after the withdrawal. /// @param payer address that provides the LP tokens to burn /// @param receiver address that receives the withdrawn tokens /// @param lpAmount amount of LP tokens to burn (proportional withdrawal) @@ -341,12 +370,13 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { ---------------------- */ /// @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 i, - uint256 j, + uint256 inputTokenIndex, + uint256 outputTokenIndex, uint256 maxAmountIn, int128 limitPrice ) @@ -362,7 +392,7 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { ) { uint256 n = tokens.length; - require(i < n && j < n, "swap: idx"); + require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx"); require(maxAmountIn > 0, "swap: input zero"); require(lmsr.nAssets > 0, "swap: empty pool"); @@ -370,18 +400,18 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { (, uint256 netUintForSwap) = _computeFee(maxAmountIn); // Convert to internal (floor) - int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, bases[i]); + 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 console2.log('stablepair optimization?', _stablePair); (amountInInternalUsed, amountOutInternal) = - _stablePair ? LMSRStabilizedBalancedPair.swapAmountsForExactInput(lmsr, i, j, deltaInternalI, limitPrice) - : lmsr.swapAmountsForExactInput(i, j, deltaInternalI, limitPrice); + _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[i]); + amountInUintNoFee = _internalToUintCeil(amountInInternalUsed, bases[inputTokenIndex]); require(amountInUintNoFee > 0, "swap: input zero"); // Compute gross transfer including fee on the used input (ceil) @@ -396,17 +426,18 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { require(grossIn <= maxAmountIn, "swap: transfer exceeds max"); // Compute output (floor) - amountOutUint = _internalToUintFloor(amountOutInternal, bases[j]); + 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 i, - uint256 j, + uint256 inputTokenIndex, + uint256 outputTokenIndex, int128 limitPrice ) internal @@ -421,15 +452,15 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { ) { uint256 n = tokens.length; - require(i < n && j < n, "swapToLimit: idx"); + 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(i, j, limitPrice); + (amountInInternal, amountOutInternal) = lmsr.swapAmountsForPriceLimit(inputTokenIndex, outputTokenIndex, limitPrice); // Convert input to uint (ceil) and output to uint (floor) - amountInUintNoFee = _internalToUintCeil(amountInInternal, bases[i]); + amountInUintNoFee = _internalToUintCeil(amountInInternal, bases[inputTokenIndex]); require(amountInUintNoFee > 0, "swapToLimit: input zero"); feeUint = 0; @@ -439,37 +470,39 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { grossIn += feeUint; } - amountOutUint = _internalToUintFloor(amountOutInternal, bases[j]); + amountOutUint = _internalToUintFloor(amountOutInternal, bases[outputTokenIndex]); require(amountOutUint > 0, "swapToLimit: output zero"); } - /// @notice External view to quote exact-in swap amounts (gross input incl. fee and output), matching swap() computations + /// @inheritdoc IPartyPool function swapAmounts( - uint256 i, - uint256 j, + uint256 inputTokenIndex, + uint256 outputTokenIndex, uint256 maxAmountIn, int128 limitPrice ) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) { - (uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(i, j, maxAmountIn, limitPrice); + (uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice); return (grossIn, outUint, feeUint); } - /// @notice External view to quote swap-to-limit amounts (gross input incl. fee and output), matching swapToLimit() computations + /// @inheritdoc IPartyPool function swapToLimitAmounts( - uint256 i, - uint256 j, + uint256 inputTokenIndex, + uint256 outputTokenIndex, int128 limitPrice ) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) { - (uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapToLimit(i, j, limitPrice); + (uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapToLimit(inputTokenIndex, outputTokenIndex, limitPrice); return (grossIn, outUint, feeUint); } /// @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. /// @param payer address of the account that pays for the swap /// @param receiver address that will receive the output tokens - /// @param i index of input asset - /// @param j index of output asset + /// @param inputTokenIndex index of input asset + /// @param outputTokenIndex index of output asset /// @param maxAmountIn maximum amount of token i (uint256) to transfer in (inclusive of fees) /// @param limitPrice maximum acceptable marginal price (64.64 fixed point). Pass 0 to ignore. /// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore. @@ -477,96 +510,97 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { function swap( address payer, address receiver, - uint256 i, - uint256 j, + uint256 inputTokenIndex, + uint256 outputTokenIndex, uint256 maxAmountIn, int128 limitPrice, uint256 deadline ) external nonReentrant returns (uint256 amountIn, uint256 amountOut, uint256 fee) { uint256 n = tokens.length; - require(i < n && j < n, "swap: idx"); + 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[i]).balanceOf(address(this)); - uint256 prevBalJ = IERC20(tokens[j]).balanceOf(address(this)); + 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(i, j, maxAmountIn, limitPrice); + _quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice); // Transfer the exact amount from payer and require exact receipt (revert on fee-on-transfer) - _safeTransferFrom(tokens[i], payer, address(this), totalTransferAmount); - uint256 balIAfter = IERC20(tokens[i]).balanceOf(address(this)); + _safeTransferFrom(tokens[inputTokenIndex], 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 - _safeTransfer(tokens[j], receiver, amountOutUint); - uint256 balJAfter = IERC20(tokens[j]).balanceOf(address(this)); + _safeTransfer(tokens[outputTokenIndex], 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[i] = balIAfter; - cachedUintBalances[j] = balJAfter; + cachedUintBalances[inputTokenIndex] = balIAfter; + cachedUintBalances[outputTokenIndex] = balJAfter; // Apply swap to LMSR state with the internal amounts actually used - lmsr.applySwap(i, j, amountInInternalUsed, amountOutInternal); + lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalUsed, amountOutInternal); - emit Swap(payer, receiver, tokens[i], tokens[j], totalTransferAmount, amountOutUint); + emit Swap(payer, receiver, tokens[inputTokenIndex], tokens[outputTokenIndex], totalTransferAmount, amountOutUint); return (totalTransferAmount, amountOutUint, feeUint); } /// @notice Swap up to the price limit; computes max input to reach limit then performs swap. - /// If the pool can't fill entirely because of balances, it caps appropriately and returns actuals. - /// Payer must approve token i for the exact computed input amount. + /// @dev If balances prevent fully reaching the limit, the function caps and returns actuals. + /// The payer must transfer the exact gross input computed by the view. /// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore. function swapToLimit( address payer, address receiver, - uint256 i, - uint256 j, + uint256 inputTokenIndex, + uint256 outputTokenIndex, int128 limitPrice, uint256 deadline ) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) { uint256 n = tokens.length; - require(i < n && j < n, "swapToLimit: idx"); + 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[i]).balanceOf(address(this)); - uint256 prevBalJ = IERC20(tokens[j]).balanceOf(address(this)); + 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(i, j, limitPrice); + _quoteSwapToLimit(inputTokenIndex, outputTokenIndex, limitPrice); // Transfer the exact amount needed from payer and require exact receipt (revert on fee-on-transfer) - _safeTransferFrom(tokens[i], payer, address(this), totalTransferAmount); - uint256 balIAfter = IERC20(tokens[i]).balanceOf(address(this)); + _safeTransferFrom(tokens[inputTokenIndex], 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 - _safeTransfer(tokens[j], receiver, amountOutUint); - uint256 balJAfter = IERC20(tokens[j]).balanceOf(address(this)); + _safeTransfer(tokens[outputTokenIndex], receiver, amountOutUint); + uint256 balJAfter = IERC20(tokens[outputTokenIndex]).balanceOf(address(this)); require(balJAfter == prevBalJ - amountOutUint, "swapToLimit: non-standard tokenOut"); // Update caches to actual balances - cachedUintBalances[i] = balIAfter; - cachedUintBalances[j] = balJAfter; + cachedUintBalances[inputTokenIndex] = balIAfter; + cachedUintBalances[outputTokenIndex] = balJAfter; // Apply swap to LMSR state with the internal amounts - lmsr.applySwap(i, j, amountInInternalMax, amountOutInternal); + lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalMax, amountOutInternal); // Maintain original event semantics (logs input without fee) - emit Swap(payer, receiver, tokens[i], tokens[j], amountInUsedUint, amountOutUint); + emit Swap(payer, receiver, tokens[inputTokenIndex], tokens[outputTokenIndex], amountInUsedUint, amountOutUint); return (amountInUsedUint, amountOutUint, feeUint); } /// @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 @@ -595,21 +629,23 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { // 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. /// @param payer who transfers the input token /// @param receiver who receives the minted LP tokens - /// @param i index of the input token + /// @param inputTokenIndex index of the input token /// @param maxAmountIn maximum uint token input (inclusive of fee) /// @param deadline optional deadline /// @return lpMinted actual LP minted (uint) function swapMint( address payer, address receiver, - uint256 i, + uint256 inputTokenIndex, uint256 maxAmountIn, uint256 deadline ) external nonReentrant returns (uint256 lpMinted) { uint256 n = tokens.length; - require(i < n, "swapMint: idx"); + require(inputTokenIndex < n, "swapMint: idx"); require(maxAmountIn > 0, "swapMint: input zero"); require(deadline == 0 || block.timestamp <= deadline, "swapMint: deadline"); @@ -620,14 +656,14 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { (, uint256 netUintGuess) = _computeFee(maxAmountIn); // Convert the net guess to internal (floor) - int128 netInternalGuess = _uintToInternalFloor(netUintGuess, bases[i]); + 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(i, netInternalGuess); + (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[i]); + 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) @@ -636,13 +672,13 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { 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[i]).balanceOf(address(this)); - _safeTransferFrom(tokens[i], payer, address(this), totalTransfer); - uint256 balIAfter = IERC20(tokens[i]).balanceOf(address(this)); + uint256 prevBalI = IERC20(tokens[inputTokenIndex]).balanceOf(address(this)); + _safeTransferFrom(tokens[inputTokenIndex], 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 i (only i changed externally) - cachedUintBalances[i] = balIAfter; + // 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); @@ -679,13 +715,13 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { // Update cached internal and kappa via updateForProportionalChange lmsr.updateForProportionalChange(newQInternal); - // Note: we updated cachedUintBalances[i] above via reading balance; other token uint balances did not + // 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, i, totalTransfer, amountInUint, feeUintActual); + 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); @@ -694,22 +730,23 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { return actualLpToMint; } - /// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `i` and send to receiver. + /// @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 i index of target asset to receive + /// @param inputTokenIndex index of target asset to receive /// @param deadline optional deadline /// @return amountOutUint uint amount of asset i sent to receiver function burnSwap( address payer, address receiver, uint256 lpAmount, - uint256 i, + uint256 inputTokenIndex, uint256 deadline ) external nonReentrant returns (uint256 amountOutUint) { uint256 n = tokens.length; - require(i < n, "burnSwap: idx"); + require(inputTokenIndex < n, "burnSwap: idx"); require(lpAmount > 0, "burnSwap: zero lp"); require(deadline == 0 || block.timestamp <= deadline, "burnSwap: deadline"); @@ -721,14 +758,14 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { int128 alpha = ABDKMath64x64.divu(lpAmount, supply); // Use LMSR view to compute single-asset payout and burned size-metric - (int128 payoutInternal, ) = lmsr.swapAmountsForBurn(i, alpha); + (int128 payoutInternal, ) = lmsr.swapAmountsForBurn(inputTokenIndex, alpha); // Convert payoutInternal -> uint (floor) to favor pool - amountOutUint = _internalToUintFloor(payoutInternal, bases[i]); + amountOutUint = _internalToUintFloor(payoutInternal, bases[inputTokenIndex]); require(amountOutUint > 0, "burnSwap: output zero"); // Transfer the payout to receiver - _safeTransfer(tokens[i], receiver, amountOutUint); + _safeTransfer(tokens[inputTokenIndex], receiver, amountOutUint); // Burn LP tokens from payer (authorization via allowance) if (msg.sender != payer) { @@ -747,7 +784,7 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { } // Emit BurnSwap with public-facing info only (do not expose ΔS or LP burned) - emit BurnSwap(payer, receiver, i, amountOutUint); + emit BurnSwap(payer, receiver, inputTokenIndex, amountOutUint); // If entire pool drained, deinit; else update proportionally bool allZero = true; @@ -765,6 +802,7 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { } + /// @inheritdoc IPartyPool function flashRepaymentAmounts(uint256[] memory loanAmounts) external view returns (uint256[] memory repaymentAmounts) { repaymentAmounts = new uint256[](tokens.length); @@ -777,10 +815,12 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { } - /// @notice Receive token0 and/or token1 and pay it back, plus a fee, in the callback - /// @dev The caller of this method receives a callback in the form of IPartyFlashCallback#partyFlashCallback + /// @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 + /// for each borrowed token. Reverts if repayment (including fee) did not occur. /// @param recipient The address which will receive the token amounts - /// @param amounts The amount of each token to send + /// @param amounts The amount of each token to send (array length must equal pool size) /// @param data Any data to be passed through to the callback function flash( address recipient, @@ -886,6 +926,7 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { } /// @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; ) {