price() and poolPrice() fixes

This commit is contained in:
tim
2025-11-06 15:04:01 -04:00
parent 2b941e0969
commit de108cc1e4
4 changed files with 67 additions and 31 deletions

View File

@@ -94,6 +94,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
/// @inheritdoc IPartyPool /// @inheritdoc IPartyPool
function denominators() external view returns (uint256[] memory) { return _bases; } function denominators() external view returns (uint256[] memory) { return _bases; }
/// @inheritdoc IPartyPool
function LMSR() external view returns (LMSRStabilized.State memory) { return _lmsr; } 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); bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
return abi.decode(result, (uint256,uint256 )); return abi.decode(result, (uint256,uint256));
} }

View File

@@ -139,13 +139,13 @@ contract PartyPoolMintImpl is PartyPoolBase {
return actualLpToMint; return actualLpToMint;
} }
/// @notice Burn LP _tokens and withdraw the proportional basket to receiver. Functional even if the pool has been /// @notice Burn LP tokens and withdraw the proportional basket to receiver. Functional even if the pool
/// killed. /// has been killed.
/// @dev Payer must own or approve the LP _tokens being burned. The function updates LMSR state /// @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. /// proportionally to reflect the reduced pool size after the withdrawal.
/// @param payer address that provides the LP _tokens to burn /// @param payer address that provides the LP tokens to burn
/// @param receiver address that receives the withdrawn _tokens /// @param receiver address that receives the withdrawn tokens
/// @param lpAmount amount of LP _tokens to burn (proportional withdrawal) /// @param lpAmount amount of LP tokens to burn (proportional withdrawal)
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore. /// @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 /// @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 function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline, bool unwrap) external nonReentrant

View File

@@ -1,14 +1,15 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30; pragma solidity ^0.8.30;
import "forge-std/console2.sol";
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol"; import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {IPartyPool} from "./IPartyPool.sol"; import {IPartyPool} from "./IPartyPool.sol";
import {IPartyPoolViewer} from "./IPartyPoolViewer.sol";
import {LMSRStabilized} from "./LMSRStabilized.sol"; import {LMSRStabilized} from "./LMSRStabilized.sol";
import {PartyPoolHelpers} from "./PartyPoolHelpers.sol"; import {PartyPoolHelpers} from "./PartyPoolHelpers.sol";
import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol"; import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol";
import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol"; import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol";
import {IPartyPoolViewer} from "./IPartyPoolViewer.sol";
contract PartyPoolViewer is PartyPoolHelpers, IPartyPoolViewer { contract PartyPoolViewer is PartyPoolHelpers, IPartyPoolViewer {
using ABDKMath64x64 for int128; using ABDKMath64x64 for int128;
@@ -36,7 +37,11 @@ contract PartyPoolViewer is PartyPoolHelpers, IPartyPoolViewer {
uint256 nAssets = lmsr.qInternal.length; uint256 nAssets = lmsr.qInternal.length;
require(nAssets > 0, "price: uninit"); require(nAssets > 0, "price: uninit");
require(baseTokenIndex < nAssets && quoteTokenIndex < nAssets, "price: idx"); 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. /// @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"); require(quoteTokenIndex < nAssets, "poolPrice: idx");
// price per unit of qTotal (Q64.64) from LMSR // price per unit of qTotal (Q64.64) from LMSR
int128 pricePerQ = LMSRStabilized.poolPrice( pool.kappa(), lmsr.qInternal, quoteTokenIndex); return 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);
} }
@@ -171,11 +161,9 @@ contract PartyPoolViewer is PartyPoolHelpers, IPartyPoolViewer {
return IERC20(token).balanceOf(address(pool)); return IERC20(token).balanceOf(address(pool));
} }
/** /// @dev The fee to be charged for a given loan.
* @dev The fee to be charged for a given loan. /// @param amount The amount of _tokens lent.
* @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.
* @return fee The amount of `token` to be charged for the loan, on top of the returned principal.
*/
function flashFee( function flashFee(
IPartyPool pool, IPartyPool pool,
address /*token*/, address /*token*/,

View File

@@ -176,7 +176,7 @@ contract PartyPoolTest is Test {
token2.transfer(address(pool), INIT_BAL); token2.transfer(address(pool), INIT_BAL);
// Perform initial mint (initial deposit); receiver is this contract // 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 // Set up pool10 with 10 _tokens
IERC20[] memory tokens10 = new IERC20[](10); IERC20[] memory tokens10 = new IERC20[](10);
@@ -1124,5 +1124,52 @@ contract PartyPoolTest is Test {
vm.stopPrank(); 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 */ /* solhint-enable */