diff --git a/src/PartyPool.sol b/src/PartyPool.sol index 0a4da2b..8c6f177 100644 --- a/src/PartyPool.sol +++ b/src/PartyPool.sol @@ -94,6 +94,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool /// @inheritdoc IPartyPool function denominators() external view returns (uint256[] memory) { return _bases; } + /// @inheritdoc IPartyPool function LMSR() external view returns (LMSRStabilized.State memory) { return _lmsr; } @@ -430,7 +431,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool ); bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data); - return abi.decode(result, (uint256,uint256 )); + return abi.decode(result, (uint256,uint256)); } diff --git a/src/PartyPoolMintImpl.sol b/src/PartyPoolMintImpl.sol index 11b5620..5acce5f 100644 --- a/src/PartyPoolMintImpl.sol +++ b/src/PartyPoolMintImpl.sol @@ -139,13 +139,13 @@ contract PartyPoolMintImpl is PartyPoolBase { return actualLpToMint; } - /// @notice Burn LP _tokens and withdraw the proportional basket to receiver. Functional even if the pool has been - /// killed. - /// @dev Payer must own or approve the LP _tokens being burned. The function updates LMSR state + /// @notice Burn LP tokens and withdraw the proportional basket to receiver. Functional even if the pool + /// has been killed. + /// @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) + /// @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) /// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore. /// @param unwrap if true and the native token is being withdrawn, it is unwraped and sent as native currency function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline, bool unwrap) external nonReentrant diff --git a/src/PartyPoolViewer.sol b/src/PartyPoolViewer.sol index 0ea42d6..b0e2776 100644 --- a/src/PartyPoolViewer.sol +++ b/src/PartyPoolViewer.sol @@ -1,14 +1,15 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.30; +import "forge-std/console2.sol"; import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol"; import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {IPartyPool} from "./IPartyPool.sol"; +import {IPartyPoolViewer} from "./IPartyPoolViewer.sol"; import {LMSRStabilized} from "./LMSRStabilized.sol"; import {PartyPoolHelpers} from "./PartyPoolHelpers.sol"; import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol"; import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol"; -import {IPartyPoolViewer} from "./IPartyPoolViewer.sol"; contract PartyPoolViewer is PartyPoolHelpers, IPartyPoolViewer { using ABDKMath64x64 for int128; @@ -36,7 +37,11 @@ contract PartyPoolViewer is PartyPoolHelpers, IPartyPoolViewer { uint256 nAssets = lmsr.qInternal.length; require(nAssets > 0, "price: uninit"); require(baseTokenIndex < nAssets && quoteTokenIndex < nAssets, "price: idx"); - return LMSRStabilized.price(pool.kappa(), lmsr.qInternal, baseTokenIndex, quoteTokenIndex); + int128 internalPrice = LMSRStabilized.price(pool.kappa(), lmsr.qInternal, baseTokenIndex, quoteTokenIndex); + // Convert to external units + uint256 bd = pool.denominators()[baseTokenIndex]; + uint256 qd = pool.denominators()[quoteTokenIndex]; + return internalPrice.mul(ABDKMath64x64.divu(bd, qd)); } /// @notice Price of one LP token denominated in `quote` as Q64.64. @@ -52,22 +57,7 @@ contract PartyPoolViewer is PartyPoolHelpers, IPartyPoolViewer { require(quoteTokenIndex < nAssets, "poolPrice: idx"); // price per unit of qTotal (Q64.64) from LMSR - int128 pricePerQ = LMSRStabilized.poolPrice( 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); + return LMSRStabilized.poolPrice( pool.kappa(), lmsr.qInternal, quoteTokenIndex); } @@ -171,11 +161,9 @@ contract PartyPoolViewer is PartyPoolHelpers, IPartyPoolViewer { return IERC20(token).balanceOf(address(pool)); } - /** - * @dev The fee to be charged for a given loan. - * @param amount The amount of _tokens lent. - * @return fee The amount of `token` to be charged for the loan, on top of the returned principal. - */ + /// @dev The fee to be charged for a given loan. + /// @param amount The amount of _tokens lent. + /// @return fee The amount of `token` to be charged for the loan, on top of the returned principal. function flashFee( IPartyPool pool, address /*token*/, diff --git a/test/PartyPool.t.sol b/test/PartyPool.t.sol index ee88055..3f61d7c 100644 --- a/test/PartyPool.t.sol +++ b/test/PartyPool.t.sol @@ -176,7 +176,7 @@ contract PartyPoolTest is Test { token2.transfer(address(pool), INIT_BAL); // Perform initial mint (initial deposit); receiver is this contract - pool.initialMint(address(this), 0); + pool.initialMint(address(this), INIT_BAL * tokens.length * 10**18); // Set up pool10 with 10 _tokens IERC20[] memory tokens10 = new IERC20[](10); @@ -1124,5 +1124,52 @@ contract PartyPoolTest is Test { vm.stopPrank(); } + /// @notice Verify that the initial relative price between token0 and token1 is 1.0000000 + function testInitialPriceIsOne() public view { + // Query the viewer for the relative price between token index 0 and 1 + int128 price = viewer.price(pool, 0, 1); + // Expected price is 1.0 in ABDK 64.64 fixed point + int128 expected = ABDKMath64x64.fromInt(1); + + // Cast int128 to uint128 then to uint256 for assertEq (values are non-negative) + assertEq(uint256(uint128(price)), uint256(uint128(expected)), "Initial relative price must be 1.0000000"); + } + + /// @notice Verify that the initial LP price in terms of token0 is 1.0000000 + function testInitialPoolPriceIsOne() public { + // Query the viewer for the pool price for token0 + int128 price = viewer.poolPrice(pool, 0); + // Expected price is 1.0 in ABDK 64.64 fixed point + int128 expected = ABDKMath64x64.fromInt(1); + + // Cast int128 to uint128 then to uint256 for assertEq (values are non-negative) + assertEq(uint256(uint128(price)), uint256(uint128(expected)), "Initial pool price must be 1.0000000"); + + // Mint a small amount of LP into the pool from alice and verify price remains 1.0 + vm.startPrank(alice); + // Approve tokens for pool to pull + token0.approve(address(pool), type(uint256).max); + token1.approve(address(pool), type(uint256).max); + token2.approve(address(pool), type(uint256).max); + + // Choose a small LP request (1% of supply or at least 1) + uint256 lpRequest = pool.totalSupply() / 100; + if (lpRequest == 0) lpRequest = 1; + + // Compute required deposits and perform mint if not trivial + uint256[] memory deposits = viewer.mintAmounts(pool, lpRequest); + bool allZero = true; + for (uint i = 0; i < deposits.length; i++) { if (deposits[i] != 0) { allZero = false; break; } } + + if (!allZero) { + pool.mint(alice, alice, lpRequest, 0); + } + vm.stopPrank(); + + // Re-query the pool price and ensure it remains 1.0 (within exact fixed-point equality) + int128 priceAfter = viewer.poolPrice(pool, 0); + assertEq(uint256(uint128(priceAfter)), uint256(uint128(expected)), "Pool price should remain 1.0000000 after mint"); + } + } /* solhint-enable */