diff --git a/src/Deploy.sol b/src/Deploy.sol index bd33dd4..77a090c 100644 --- a/src/Deploy.sol +++ b/src/Deploy.sol @@ -7,6 +7,7 @@ import {PartyPool} from "./PartyPool.sol"; import {PartyPoolBalancedPair} from "./PartyPoolBalancedPair.sol"; import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol"; import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol"; +import {PartyPoolView} from "./PartyPoolView.sol"; library Deploy { @@ -62,4 +63,8 @@ library Deploy { ); } + + function newViewer() internal returns (PartyPoolView) { + return new PartyPoolView(new PartyPoolSwapImpl(), new PartyPoolMintImpl()); + } } diff --git a/src/IPartyPool.sol b/src/IPartyPool.sol index 663f20a..c649c09 100644 --- a/src/IPartyPool.sol +++ b/src/IPartyPool.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.30; -import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import "./LMSRStabilized.sol"; import {IERC20Metadata} from "../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; /// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token /// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model. @@ -56,6 +57,8 @@ interface IPartyPool is IERC20Metadata { ); + function LMSR() external view returns (LMSRStabilized.State memory); + /// @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 getToken(uint256) external view returns (IERC20); // get single token @@ -103,14 +106,6 @@ interface IPartyPool is IERC20Metadata { /// @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 returns (uint256 lpMinted); - /// @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 mintAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory depositAmounts); - /// @notice Proportional mint (or initial supply if first call). /// @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. @@ -122,13 +117,6 @@ interface IPartyPool is IERC20Metadata { /// @return lpMinted the actual amount of lpToken minted function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external returns (uint256 lpMinted); - /// @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 burnAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts); - /// @notice Burn LP tokens and withdraw the proportional basket to receiver. /// @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. @@ -175,17 +163,6 @@ interface IPartyPool is IERC20Metadata { 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 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. @@ -238,28 +215,6 @@ interface IPartyPool is IERC20Metadata { uint256 deadline ) external returns (uint256 amountOutUint); - /// @notice Marginal price of `base` denominated in `quote` as Q64.64. - /// @dev Returns the LMSR marginal price p_quote / p_base in ABDK 64.64 fixed-point format. - /// Useful for off-chain quoting; raw 64.64 value is returned (no scaling to token units). - /// @param baseTokenIndex index of the base asset (e.g., ETH) - /// @param quoteTokenIndex index of the quote asset (e.g., USD) - /// @return price Q64.64 value equal to quote per base (p_quote / p_base) - function price(uint256 baseTokenIndex, uint256 quoteTokenIndex) external view returns (int128); - - /// @notice Price of one LP token denominated in `quote` as Q64.64. - /// @dev Computes LMSR poolPrice (quote per unit internal qTotal) and scales it to LP units: - /// returns price_per_LP = poolPrice_quote * (totalSupply() / qTotal) in ABDK 64.64 format. - /// The returned value is raw Q64.64 and represents quote units per one LP token unit. - /// @param quoteTokenIndex index of the quote asset in which to denominate the LP price - /// @return price Q64.64 value equal to quote per LP token unit - function poolPrice(uint256 quoteTokenIndex) external view returns (int128); - - /// @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 diff --git a/src/PartyPool.sol b/src/PartyPool.sol index 7059ab4..e7b9e79 100644 --- a/src/PartyPool.sol +++ b/src/PartyPool.sol @@ -80,6 +80,9 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { /// @inheritdoc IPartyPool function denominators() external view returns (uint256[] memory) { return bases; } + function LMSR() external view returns (LMSRStabilized.State memory) { return lmsr; } + + /// @param name_ LP token name /// @param symbol_ LP token symbol /// @param tokens_ token addresses (n) @@ -87,7 +90,7 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { /// @param kappa_ liquidity parameter κ (Q64.64) used to derive b = κ * S(q) /// @param swapFeePpm_ fee in parts-per-million, taken from swap input amounts before LMSR calculations /// @param flashFeePpm_ fee in parts-per-million, taken for flash loans - /// @param swapMintImpl_ address of the SwapMint implementation contract + /// @param swapImpl_ address of the SwapMint implementation contract /// @param mintImpl_ address of the Mint implementation contract constructor( string memory name_, @@ -99,7 +102,7 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { uint256 flashFeePpm_, uint256 protocolFeePpm_, // NEW: protocol share of fees (ppm) address protocolFeeAddress_, // NEW: recipient for collected protocol tokens - PartyPoolSwapImpl swapMintImpl_, + PartyPoolSwapImpl swapImpl_, PartyPoolMintImpl mintImpl_ ) ERC20External(name_, symbol_) { require(tokens_.length > 1, "Pool: need >1 asset"); @@ -114,7 +117,7 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { require(protocolFeePpm_ < 1_000_000, "Pool: protocol fee >= ppm"); PROTOCOL_FEE_PPM = protocolFeePpm_; PROTOCOL_FEE_ADDRESS = protocolFeeAddress_; - SWAP_IMPL = swapMintImpl_; + SWAP_IMPL = swapImpl_; MINT_IMPL = mintImpl_; uint256 n = tokens_.length; @@ -134,47 +137,6 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { } - // - // Current marginal prices - // - - /// @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); - } - - /// @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); - } - - /* ---------------------- Initialization / Mint / Burn (LP token managed) ---------------------- */ @@ -192,11 +154,6 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { return abi.decode(result, (uint256)); } - /// @inheritdoc IPartyPool - function mintAmounts(uint256 lpTokenAmount) public view returns (uint256[] memory depositAmounts) { - return MINT_IMPL.mintAmounts(lpTokenAmount, lmsr.nAssets, totalSupply(), cachedUintBalances); - } - /// @notice Proportional mint for existing pool. /// @dev This function forwards the call to the mint implementation via delegatecall /// @param payer address that provides the input tokens @@ -216,11 +173,6 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { return abi.decode(result, (uint256)); } - /// @inheritdoc IPartyPool - function burnAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts) { - return MINT_IMPL.burnAmounts(lpTokenAmount, lmsr.nAssets, totalSupply(), cachedUintBalances); - } - /// @notice Burn LP tokens and withdraw the proportional basket to receiver. /// @dev This function forwards the call to the burn implementation via delegatecall /// @param payer address that provides the LP tokens to burn @@ -367,22 +319,6 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { } - /// @inheritdoc IPartyPool - function swapToLimitAmounts( - uint256 inputTokenIndex, - uint256 outputTokenIndex, - int128 limitPrice - ) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) { - require(inputTokenIndex < tokens.length && outputTokenIndex < tokens.length, "swapToLimit: idx"); - require(limitPrice > int128(0), "swapToLimit: limit <= 0"); - require(lmsr.nAssets > 0, "swapToLimit: pool uninitialized"); - - return SWAP_IMPL.swapToLimitAmounts( - inputTokenIndex, outputTokenIndex, limitPrice, - bases, KAPPA, lmsr.qInternal, SWAP_FEE_PPM); - } - - /// @inheritdoc IPartyPool function swapToLimit( address payer, @@ -407,29 +343,6 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { return abi.decode(result, (uint256,uint256,uint256)); } - function swapMintAmounts(uint256 inputTokenIndex, uint256 maxAmountIn) external view - returns (uint256 amountInUsed, uint256 fee, uint256 lpMinted) { - return MINT_IMPL.swapMintAmounts( - inputTokenIndex, - maxAmountIn, - SWAP_FEE_PPM, - lmsr, - bases, - totalSupply() - ); - } - - function burnSwapAmounts(uint256 lpAmount, uint256 inputTokenIndex) external view - returns (uint256 amountOut) { - return MINT_IMPL.burnSwapAmounts( - lpAmount, - inputTokenIndex, - SWAP_FEE_PPM, - lmsr, - bases, - totalSupply() - ); - } /// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP. /// @dev This function forwards the call to the swapMint implementation via delegatecall @@ -492,19 +405,6 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { } - /// @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, FLASH_FEE_PPM); - } - } - } - - /// @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 @@ -604,7 +504,6 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { } } - function _swapAmountsForExactInput(uint256 i, uint256 j, int128 a, int128 limitPrice) internal virtual view returns (int128 amountIn, int128 amountOut) { return lmsr.swapAmountsForExactInput(i, j, a, limitPrice); diff --git a/src/PartyPoolBase.sol b/src/PartyPoolBase.sol index 9bcad97..6886cd6 100644 --- a/src/PartyPoolBase.sol +++ b/src/PartyPoolBase.sol @@ -6,10 +6,11 @@ import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20 import {ReentrancyGuard} from "../lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol"; import {ERC20Internal} from "./ERC20Internal.sol"; import {LMSRStabilized} from "./LMSRStabilized.sol"; +import {PartyPoolHelpers} from "./PartyPoolHelpers.sol"; /// @notice Abstract base contract that contains storage and internal helpers only. /// No external/public functions or constructor here — derived implementations own immutables and constructors. -abstract contract PartyPoolBase is ERC20Internal, ReentrancyGuard { +abstract contract PartyPoolBase is ERC20Internal, ReentrancyGuard, PartyPoolHelpers { using ABDKMath64x64 for int128; using LMSRStabilized for LMSRStabilized.State; @@ -77,46 +78,6 @@ abstract contract PartyPoolBase is ERC20Internal, ReentrancyGuard { return floorValue; } - /// @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 Compute fee and net amounts for a gross input (fee rounded up to favor the pool). - /// @param gross total gross input - /// @param feePpm fee in ppm to apply - /// @return feeUint fee taken (uint) and netUint remaining for protocol use (uint) - function _computeFee(uint256 gross, uint256 feePpm) internal pure returns (uint256 feeUint, uint256 netUint) { - if (feePpm == 0) { - return (0, gross); - } - feeUint = _ceilFee(gross, feePpm); - netUint = gross - feeUint; - } - - /// @notice Convenience: return gross = net + fee(net) using ceiling for fee. - /// @param netUint net amount - /// @param feePpm fee in ppm to apply - function _addFee(uint256 netUint, uint256 feePpm) internal pure returns (uint256 gross) { - if (feePpm == 0) return netUint; - uint256 fee = _ceilFee(netUint, feePpm); - return netUint + fee; - } - - /// @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_) internal pure returns (int128) { - int128 total = int128(0); - for (uint i = 0; i < qInternal_.length; ) { - total = total.add(qInternal_[i]); - unchecked { i++; } - } - return total; - } - /// @dev Helper to record cached balances as effectiveBalance = onchain - owed. Reverts if owed > onchain. function _recordCachedBalance(uint256 idx, uint256 onchainBal) internal { uint256 owed = protocolFeesOwed[idx]; diff --git a/src/PartyPoolHelpers.sol b/src/PartyPoolHelpers.sol new file mode 100644 index 0000000..dafecf8 --- /dev/null +++ b/src/PartyPoolHelpers.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.30; + +import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol"; + + +abstract contract PartyPoolHelpers { + using ABDKMath64x64 for int128; + + /// @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 Compute fee and net amounts for a gross input (fee rounded up to favor the pool). + /// @param gross total gross input + /// @param feePpm fee in ppm to apply + /// @return feeUint fee taken (uint) and netUint remaining for protocol use (uint) + function _computeFee(uint256 gross, uint256 feePpm) internal pure returns (uint256 feeUint, uint256 netUint) { + if (feePpm == 0) { + return (0, gross); + } + feeUint = _ceilFee(gross, feePpm); + netUint = gross - feeUint; + } + + /// @notice Convenience: return gross = net + fee(net) using ceiling for fee. + /// @param netUint net amount + /// @param feePpm fee in ppm to apply + function _addFee(uint256 netUint, uint256 feePpm) internal pure returns (uint256 gross) { + if (feePpm == 0) return netUint; + uint256 fee = _ceilFee(netUint, feePpm); + return netUint + fee; + } + + /// @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_) internal pure returns (int128) { + int128 total = int128(0); + for (uint i = 0; i < qInternal_.length; ) { + total = total.add(qInternal_[i]); + unchecked { i++; } + } + return total; + } + +} \ No newline at end of file diff --git a/src/PartyPoolMintImpl.sol b/src/PartyPoolMintImpl.sol index ff5fcf9..89ac27e 100644 --- a/src/PartyPoolMintImpl.sol +++ b/src/PartyPoolMintImpl.sol @@ -17,9 +17,6 @@ contract PartyPoolMintImpl is PartyPoolBase { using LMSRStabilized for LMSRStabilized.State; using SafeERC20 for IERC20; - // 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); // // Initialization Mint @@ -57,7 +54,7 @@ contract PartyPoolMintImpl is PartyPoolBase { require(lpMinted > 0, "initialMint: zero LP amount"); _mint(receiver, lpMinted); - emit Mint(address(0), receiver, depositAmounts, lpMinted); + emit IPartyPool.Mint(address(0), receiver, depositAmounts, lpMinted); } @@ -126,7 +123,7 @@ contract PartyPoolMintImpl is PartyPoolBase { require(actualLpToMint >= minAcceptable, "mint: insufficient LP minted"); _mint(receiver, actualLpToMint); - emit Mint(payer, receiver, depositAmounts, actualLpToMint); + emit IPartyPool.Mint(payer, receiver, depositAmounts, actualLpToMint); return actualLpToMint; } @@ -199,9 +196,15 @@ contract PartyPoolMintImpl is PartyPoolBase { } _burn(payer, lpAmount); - emit Burn(payer, receiver, withdrawAmounts, lpAmount); + emit IPartyPool.Burn(payer, receiver, withdrawAmounts, lpAmount); } + /// @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 mintAmounts(uint256 lpTokenAmount, uint256 numAssets, uint256 totalSupply, uint256[] memory cachedUintBalances) public pure returns (uint256[] memory depositAmounts) { diff --git a/src/PartyPoolView.sol b/src/PartyPoolView.sol new file mode 100644 index 0000000..d56da86 --- /dev/null +++ b/src/PartyPoolView.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.30; + +import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol"; +import {IPartyPool} from "./IPartyPool.sol"; +import {LMSRStabilized} from "./LMSRStabilized.sol"; +import {PartyPoolHelpers} from "./PartyPoolHelpers.sol"; +import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol"; +import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol"; + +contract PartyPoolView is PartyPoolHelpers { + using ABDKMath64x64 for int128; + + PartyPoolSwapImpl immutable internal SWAP_IMPL; + PartyPoolMintImpl immutable internal MINT_IMPL; + + constructor(PartyPoolSwapImpl swapImpl_, PartyPoolMintImpl mintImpl) { + SWAP_IMPL = swapImpl_; + MINT_IMPL = mintImpl; + } + + // + // Current marginal prices + // + + /// @notice Marginal price of `base` denominated in `quote` as Q64.64. + /// @dev Returns the LMSR marginal price p_quote / p_base in ABDK 64.64 fixed-point format. + /// Useful for off-chain quoting; raw 64.64 value is returned (no scaling to token units). + /// @param baseTokenIndex index of the base asset (e.g., ETH) + /// @param quoteTokenIndex index of the quote asset (e.g., USD) + /// @return price Q64.64 value equal to quote per base (p_quote / p_base) + function price(IPartyPool pool, uint256 baseTokenIndex, uint256 quoteTokenIndex) external view returns (int128) { + LMSRStabilized.State memory lmsr = pool.LMSR(); + require(baseTokenIndex < lmsr.nAssets && quoteTokenIndex < lmsr.nAssets, "price: idx"); + require(lmsr.nAssets > 0, "price: uninit"); + return LMSRStabilized.price(lmsr.nAssets, pool.kappa(), lmsr.qInternal, baseTokenIndex, quoteTokenIndex); + } + + /// @notice Price of one LP token denominated in `quote` as Q64.64. + /// @dev Computes LMSR poolPrice (quote per unit internal qTotal) and scales it to LP units: + /// returns price_per_LP = poolPrice_quote * (totalSupply() / qTotal) in ABDK 64.64 format. + /// The returned value is raw Q64.64 and represents quote units per one LP token unit. + /// @param quoteTokenIndex index of the quote asset in which to denominate the LP price + /// @return price Q64.64 value equal to quote per LP token unit + function poolPrice(IPartyPool pool, uint256 quoteTokenIndex) external view returns (int128) { + LMSRStabilized.State memory lmsr = pool.LMSR(); + require(lmsr.nAssets > 0, "poolPrice: uninit"); + require(quoteTokenIndex < lmsr.nAssets, "poolPrice: idx"); + + // price per unit of qTotal (Q64.64) from LMSR + int128 pricePerQ = LMSRStabilized.poolPrice(lmsr.nAssets, pool.kappa(), lmsr.qInternal, quoteTokenIndex); + + // total internal q (qTotal) as Q64.64 + int128 qTotal = LMSRStabilized._computeSizeMetric(lmsr.qInternal); + require(qTotal > int128(0), "poolPrice: qTotal zero"); + + // totalSupply as Q64.64 + uint256 supply = pool.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); + } + + + function mintAmounts(IPartyPool pool, uint256 lpTokenAmount) public view returns (uint256[] memory depositAmounts) { + LMSRStabilized.State memory lmsr = pool.LMSR(); + uint256[] memory cachedUintBalances = new uint256[](lmsr.nAssets); + for( uint256 i=0; i 0) + /// @return amountIn gross input amount to transfer (includes fee), amountOut output amount user would receive, fee fee amount taken + function swapToLimitAmounts( + IPartyPool pool, + uint256 inputTokenIndex, + uint256 outputTokenIndex, + int128 limitPrice + ) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) { + LMSRStabilized.State memory lmsr = pool.LMSR(); + require(inputTokenIndex < lmsr.nAssets && outputTokenIndex < lmsr.nAssets, "swapToLimit: idx"); + require(limitPrice > int128(0), "swapToLimit: limit <= 0"); + require(lmsr.nAssets > 0, "swapToLimit: pool uninitialized"); + + return SWAP_IMPL.swapToLimitAmounts( + inputTokenIndex, outputTokenIndex, limitPrice, + pool.denominators(), pool.kappa(), lmsr.qInternal, pool.swapFeePpm()); + } + + + function swapMintAmounts(IPartyPool pool, uint256 inputTokenIndex, uint256 maxAmountIn) external view + returns (uint256 amountInUsed, uint256 fee, uint256 lpMinted) { + LMSRStabilized.State memory lmsr = pool.LMSR(); + return MINT_IMPL.swapMintAmounts( + inputTokenIndex, + maxAmountIn, + pool.swapFeePpm(), + lmsr, + pool.denominators(), + pool.totalSupply() + ); + } + + + function burnSwapAmounts(IPartyPool pool, uint256 lpAmount, uint256 inputTokenIndex) external view + returns (uint256 amountOut) { + LMSRStabilized.State memory lmsr = pool.LMSR(); + return MINT_IMPL.burnSwapAmounts( + lpAmount, + inputTokenIndex, + pool.swapFeePpm(), + lmsr, + pool.denominators(), + pool.totalSupply() + ); + } + + + /// @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(IPartyPool pool, uint256[] memory loanAmounts) external view + returns (uint256[] memory repaymentAmounts) { + LMSRStabilized.State memory lmsr = pool.LMSR(); + repaymentAmounts = new uint256[](lmsr.nAssets); + for (uint256 i = 0; i < lmsr.nAssets; i++) { + uint256 amount = loanAmounts[i]; + if (amount > 0) { + repaymentAmounts[i] = amount + _ceilFee(amount, pool.flashFeePpm()); + } + } + } + + +} diff --git a/test/PartyPool.t.sol b/test/PartyPool.t.sol index 7036816..a8da3d5 100644 --- a/test/PartyPool.t.sol +++ b/test/PartyPool.t.sol @@ -12,6 +12,7 @@ import "../src/PartyPool.sol"; import "../src/IPartyFlashCallback.sol"; import {PartyPlanner} from "../src/PartyPlanner.sol"; import {Deploy} from "../src/Deploy.sol"; +import {PartyPoolView} from "../src/PartyPoolView.sol"; /// @notice Test contract that implements the flash callback for testing flash loans contract FlashBorrower is IPartyFlashCallback { @@ -143,6 +144,7 @@ contract PartyPoolTest is Test { PartyPlanner planner; PartyPool pool; PartyPool pool10; + PartyPoolView viewer; address alice; address bob; @@ -283,6 +285,8 @@ contract PartyPoolTest is Test { token7.mint(bob, INIT_BAL); token8.mint(bob, INIT_BAL); token9.mint(bob, INIT_BAL); + + viewer = Deploy.newViewer(); } /// @notice Basic sanity: initial mint should have produced LP tokens for this contract and the pool holds tokens. @@ -324,7 +328,7 @@ contract PartyPoolTest is Test { token2.approve(address(pool), type(uint256).max); // Inspect the deposit amounts that the pool will require (these are rounded up) - uint256[] memory deposits = pool.mintAmounts(1); + uint256[] memory deposits = viewer.mintAmounts(pool, 1); // Basic sanity: deposits array length must match token count and not all zero necessarily assertEq(deposits.length, 3); @@ -366,7 +370,7 @@ contract PartyPoolTest is Test { uint256 totalLpBefore = pool.totalSupply(); // Compute required deposits and perform mint for 1 wei - uint256[] memory deposits = pool.mintAmounts(1); + uint256[] memory deposits = viewer.mintAmounts(pool, 1); // Sum deposits as deposited_value uint256 depositedValue = 0; @@ -407,7 +411,7 @@ contract PartyPoolTest is Test { // Request half of LP supply uint256 want = totalLp / 2; - uint256[] memory deposits = pool.mintAmounts(want); + uint256[] memory deposits = viewer.mintAmounts(pool, want); // We expect each deposit to be roughly half the pool balance, but due to rounding up it should satisfy: // deposits[i] * 2 >= cached balance (i.e., rounding up) @@ -424,7 +428,7 @@ contract PartyPoolTest is Test { assertTrue(totalLp > 0, "precondition: LP > 0"); // Compute amounts required to redeem entire supply (should be current balances) - uint256[] memory withdrawAmounts = pool.burnAmounts(totalLp); + uint256[] memory withdrawAmounts = viewer.burnAmounts(pool, totalLp); // Sanity: withdrawAmounts should equal pool balances (or very close due to rounding) for (uint i = 0; i < withdrawAmounts.length; i++) { @@ -528,7 +532,7 @@ contract PartyPoolTest is Test { if (req == 0) req = 1; // Compute expected deposit amounts via view - uint256[] memory expected = pool.mintAmounts(req); + uint256[] memory expected = viewer.mintAmounts(pool, req); // Ensure alice has tokens and approve pool vm.startPrank(alice); @@ -573,7 +577,7 @@ contract PartyPoolTest is Test { uint256 req = requests[k]; if (req == 0) req = 1; - uint256[] memory expected = pool10.mintAmounts(req); + uint256[] memory expected = viewer.mintAmounts(pool10, req); // Approve all tokens from alice vm.startPrank(alice); @@ -651,7 +655,7 @@ contract PartyPoolTest is Test { } // Recompute withdraw amounts via view after any top-up - uint256[] memory expected = pool.burnAmounts(req); + uint256[] memory expected = viewer.burnAmounts(pool, req); // If expected withdraws are all zero (rounding edge), skip this iteration if (expected[0] == 0 && expected[1] == 0 && expected[2] == 0) { @@ -708,7 +712,7 @@ contract PartyPoolTest is Test { vm.stopPrank(); } - uint256[] memory expected = pool10.burnAmounts(req); + uint256[] memory expected = viewer.burnAmounts(pool10, req); // If expected withdraws are all zero (rounding edge), skip this iteration bool allZero = true; @@ -1169,7 +1173,7 @@ contract PartyPoolTest is Test { for (uint256 i = 0; i < testCases.length; i++) { uint256[] memory loanAmounts = testCases[i]; - uint256[] memory repaymentAmounts = pool.flashRepaymentAmounts(loanAmounts); + uint256[] memory repaymentAmounts = viewer.flashRepaymentAmounts(pool, loanAmounts); // Verify each repayment amount is correctly calculated for (uint256 j = 0; j < loanAmounts.length; j++) { @@ -1361,8 +1365,8 @@ contract PartyPoolTest is Test { token2.approve(address(poolCustom), type(uint256).max); // Get required deposit amounts for both pools - uint256[] memory depositsDefault = poolDefault.mintAmounts(lpRequestDefault); - uint256[] memory depositsCustom = poolCustom.mintAmounts(lpRequestCustom); + uint256[] memory depositsDefault = viewer.mintAmounts(poolDefault, lpRequestDefault); + uint256[] memory depositsCustom = viewer.mintAmounts(poolCustom, lpRequestCustom); // Deposits should be identical (same proportion of identical balances) assertEq(depositsDefault[0], depositsCustom[0], "Token0 deposits should be identical");