diff --git a/doc/launch_list.md b/doc/launch_list.md index 6ea61e7..99b6dba 100644 --- a/doc/launch_list.md +++ b/doc/launch_list.md @@ -32,11 +32,11 @@ | BNB | `0xB8c77482e45F1F44dE1745F52C74426C631bDD52` | nonstandard API | -# Proof of Concept Pool +# OG Pool ``` - Name: Liquidity Party POC -Symbol: POC.LP + Name: Original Genesis of Liquidity Party +Symbol: OG.LP Kappa: 0.01 ``` @@ -52,3 +52,14 @@ Symbol: POC.LP | UNI | 0.00145 | `0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984` | | PEPE | 0.00215 | | | SHIB | 0.00215 | | + +USDT 0xdAC17F958D2ee523a2206206994597C13D831ec7 +USDC 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 +WBTC 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 +WETH 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 +UNI 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984 +WSOL 0xD31a59c85aE9D8edEFeC411D448f90841571b89c +TRX 0x50327c6c5a14DCaDE707ABad2E27eB517df87AB5 +AAVE 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9 +PEPE 0x6982508145454Ce325dDbE47a25d4ec3d2311933 +SHIB 0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE \ No newline at end of file diff --git a/src/LMSRStabilized.sol b/src/LMSRStabilized.sol index 2b9372d..3f4e965 100644 --- a/src/LMSRStabilized.sol +++ b/src/LMSRStabilized.sol @@ -776,19 +776,19 @@ library LMSRStabilized { return _exp(qInternal[baseTokenIndex].sub(qInternal[quoteTokenIndex]).mul(invB)); } - /// @notice Price of one unit of the LP size-metric (S = sum q_i) denominated in `quote` asset (Q64.64) - /// @dev Computes: poolPrice_quote = (1 / S) * sum_j q_j * exp((q_j - q_quote) / b) - function poolPrice(State storage s, uint256 quoteTokenIndex) internal view returns (int128) { - return poolPrice(s.kappa, s.qInternal, quoteTokenIndex); + /// @notice Total pool value denominated in `quote` asset (Q64.64, internal quote units) + /// @dev Computes: poolValue_quote = sum_j q_j * exp((q_j - q_quote) / b) + function poolValue(State storage s, uint256 quoteTokenIndex) internal view returns (int128) { + return poolValue(s.kappa, s.qInternal, quoteTokenIndex); } - /// @notice Pure version: Price of one unit of the LP size-metric (S = sum q_i) denominated in `quote` asset (Q64.64) - /// @dev Computes: poolPrice_quote = (1 / S) * sum_j q_j * exp((q_j - q_quote) / b) + /// @notice Pure version: Total pool value denominated in `quote` asset (Q64.64, internal quote units) + /// @dev Computes: poolValue_quote = sum_j q_j * exp((q_j - q_quote) / b) /// @param kappa Liquidity parameter κ (64.64 fixed point) /// @param qInternal Cached internal balances in 64.64 fixed-point format /// @param quoteTokenIndex Index of quote token - /// @return Pool price in 64.64 fixed-point format - function poolPrice(int128 kappa, int128[] memory qInternal, uint256 quoteTokenIndex) internal pure returns (int128) { + /// @return Total pool value in 64.64 fixed-point format (internal quote units) + function poolValue(int128 kappa, int128[] memory qInternal, uint256 quoteTokenIndex) internal pure returns (int128) { // Compute b and ensure positivity int128 sizeMetric = _computeSizeMetric(qInternal); require(sizeMetric > int128(0), "LMSR: size metric zero"); @@ -814,8 +814,7 @@ library LMSRStabilized { unchecked { j++; } } - // pool price in units of quote = (1 / S) * acc - return acc.div(S); + return acc; } /* -------------------- diff --git a/src/PartyInfo.sol b/src/PartyInfo.sol index 1bfca10..135065a 100644 --- a/src/PartyInfo.sol +++ b/src/PartyInfo.sol @@ -67,8 +67,12 @@ contract PartyInfo is PartyPoolHelpers, IPartyInfo { require(nAssets > 0, "poolPrice: uninit"); require(quoteTokenIndex < nAssets, "poolPrice: idx"); - // price per unit of qTotal (Q64.64) from LMSR - return LMSRStabilized.poolPrice( pool.kappa(), lmsr.qInternal, quoteTokenIndex); + // LMSR total value of pool in terms of quote token + int128 value = LMSRStabilized.poolValue(pool.kappa(), lmsr.qInternal, quoteTokenIndex); + + uint256 qd = pool.denominators()[quoteTokenIndex]; + uint256 supply = pool.totalSupply(); + return value.mul(ABDKMath64x64.divu(qd * 10**18, supply)); } diff --git a/src/PartyPoolMintImpl.sol b/src/PartyPoolMintImpl.sol index d009221..c1ce467 100644 --- a/src/PartyPoolMintImpl.sol +++ b/src/PartyPoolMintImpl.sol @@ -224,13 +224,13 @@ contract PartyPoolMintImpl is PartyPoolBase { return depositAmounts; // Return zeros, initial deposit handled differently } - // lpTokenAmount / totalLpSupply = depositAmount / currentBalance - // Therefore: depositAmount = (lpTokenAmount * currentBalance) / totalLpSupply - // We round up to protect the pool + // Compute mint ratio in Q64.64: ratio = lpTokenAmount / totalSupply + int128 ratio = ABDKMath64x64.divu(lpTokenAmount, totalSupply); + + // depositAmount_i = ceil(ratio * currentBalance_i) for (uint i = 0; i < numAssets; i++) { uint256 currentBalance = cachedUintBalances[i]; - // Calculate with rounding up: (a * b + c - 1) / c - depositAmounts[i] = (lpTokenAmount * currentBalance + totalSupply - 1) / totalSupply; + depositAmounts[i] = _internalToUintCeilPure(ratio, currentBalance); } return depositAmounts; @@ -247,10 +247,10 @@ contract PartyPoolMintImpl is PartyPoolBase { return withdrawAmounts; // Return zeros, nothing to withdraw } - // withdrawAmount = floor(lpTokenAmount * currentBalance / totalLpSupply) + int128 ratio = ABDKMath64x64.divu(lpTokenAmount, totalSupply); for (uint i = 0; i < numAssets; i++) { uint256 currentBalance = cachedUintBalances[i]; - withdrawAmounts[i] = (lpTokenAmount * currentBalance) / totalSupply; + withdrawAmounts[i] = ratio.mulu(currentBalance); } return withdrawAmounts; @@ -536,7 +536,6 @@ contract PartyPoolMintImpl is PartyPoolBase { IERC20 outputToken = _tokens[outputTokenIndex]; _sendTokenTo(outputToken, receiver, amountOut, unwrap); - // Update cached balances using computed payout and protocol fee; no on-chain reads int128[] memory newQInternal = new int128[](n); @@ -574,15 +573,24 @@ contract PartyPoolMintImpl is PartyPoolBase { /// @notice Pure version of _internalToUintCeil for use in view functions function _internalToUintCeilPure(int128 amount, uint256 base) internal pure returns (uint256) { // Convert Q64.64 to uint with ceiling: ceil(amount * base) - // Use mulu which floors, then add remainder check for ceiling + // Fast path: compute floor using mulu, then detect fractional remainder via low 64-bit check uint256 floored = ABDKMath64x64.mulu(amount, base); - // Check if there's a fractional part by computing amount * base - floored - int128 baseQ64 = ABDKMath64x64.fromUInt(base); - int128 flooredQ64 = ABDKMath64x64.fromUInt(floored); - int128 product = amount.mul(baseQ64); - if (product > flooredQ64) { - return floored + 1; // Ceiling + + // Extract fractional 64 bits of `amount`; if zero, product is already an integer after scaling + uint64 frac = uint64(uint128(amount)); + if (frac == 0) { + return floored; } + + unchecked { + // Remainder exists iff (frac * (base mod 2^64)) mod 2^64 != 0 + uint64 baseL = uint64(base); + uint128 low = uint128(frac) * uint128(baseL); + if (uint64(low) != 0) { + return floored + 1; // Ceiling + } + } + return floored; } diff --git a/test/LMSRStabilized.t.sol b/test/LMSRStabilized.t.sol index 51526e8..67cae5e 100644 --- a/test/LMSRStabilized.t.sol +++ b/test/LMSRStabilized.t.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; -import "forge-std/console2.sol"; import "@openzeppelin/contracts/interfaces/IERC20Metadata.sol"; import "../src/LMSRStabilized.sol"; import "../src/LMSRStabilizedBalancedPair.sol"; @@ -90,7 +89,6 @@ contract LMSRStabilizedTest is Test { int128 newE0 = eValues[0].mul(_exp(tradeAmount.div(b))); int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1])); int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1)); - console2.log('slippage', slippage); // Slippage should be close to stdSlippage (within 1% relative error) int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage); @@ -133,7 +131,6 @@ contract LMSRStabilizedTest is Test { int128 newE0 = eValues[0].mul(_exp(tradeAmount.div(b))); int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1])); int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1)); - console2.log('slippage', slippage); int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage); assertLt(relativeError, ABDKMath64x64.divu(1, 100), "Almost balanced pool slippage error too high"); } @@ -175,14 +172,12 @@ contract LMSRStabilizedTest is Test { int128 newE0 = eValues[0].mul(_exp(tradeAmount.div(b))); int128 slippageRatio = newE0.div(eValues[0]).div(eValues[2].div(eValues[2])); int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1)); - console2.log('slippage', slippage); // Since the imbalance is extreme, with one coin worth lots more than the others, the actual slippage for // this swap is actually off by about 100% // When we configure kappa, it is a best case slippage (worst case AMM loss) that only occurs with balanced // assets int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage); - console2.log('relative error', relativeError); assertLt(relativeError, ABDKMath64x64.divu(100, 100), "Imbalanced pool slippage error too high"); } @@ -249,7 +244,6 @@ contract LMSRStabilizedTest is Test { int128 newE0 = eValues[0].mul(_exp(tradeAmount.div(newB))); int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1])); int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1)); - console2.log('post-deposit slippage', slippage); int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage); assertLt(relativeError, ABDKMath64x64.divu(1, 100), "Slippage target not met after deposit"); @@ -398,7 +392,6 @@ contract LMSRStabilizedTest is Test { int128 newE0 = eValues[0].mul(_exp(tradeAmount.div(newB))); int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1])); int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1)); - console2.log('post-withdrawal slippage', slippage); int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage); assertLt(relativeError, ABDKMath64x64.divu(1, 100), "Slippage target not met after withdrawal"); @@ -712,10 +705,6 @@ contract LMSRStabilizedTest is Test { // The path independence property isn't perfect due to discrete swap mechanics, // but the difference should be within reasonable bounds - console2.log("Direct swap output:"); - console2.logInt(directAmountOut); - console2.log("Indirect swap total output:"); - console2.logInt(indirectAmountOut2); // Basic verification that both paths produce positive outputs assertTrue(directAmountOut > 0, "Direct swap should produce positive output"); @@ -736,8 +725,6 @@ contract LMSRStabilizedTest is Test { // Update the state's cached qInternal _updateCachedQInternal(initialQ); - console2.log("Testing round-trip trades for balanced pool"); - // Use standard trade size int128 tradeAmount = s.qInternal[0].mul(stdTradeSize); @@ -754,9 +741,6 @@ contract LMSRStabilizedTest is Test { // Calculate round-trip slippage: (initial amount - final amount) / initial amount int128 roundTripSlippage = (amountIn1.sub(amountOut2)).div(amountIn1); - console2.log("Round-trip slippage (micro-units):"); - console2.logInt(_toMicro(roundTripSlippage)); - // Verify round-trip slippage is reasonable int128 tolerance = ABDKMath64x64.divu(1, 100000); // 0.001% tolerance assertLt(roundTripSlippage.abs(), tolerance, "Round-trip slippage should be near zero"); @@ -797,15 +781,6 @@ contract LMSRStabilizedTest is Test { // Second direction: asset 1 -> asset 0 (int128 amountIn1to0, int128 amountOut1to0) = s.swapAmountsForExactInput(1, 0, tradeAmount1, 0); - console2.log("0->1 swap amountIn:"); - console2.logInt(amountIn0to1); - console2.log("0->1 swap amountOut:"); - console2.logInt(amountOut0to1); - console2.log("1->0 swap amountIn:"); - console2.logInt(amountIn1to0); - console2.log("1->0 swap amountOut:"); - console2.logInt(amountOut1to0); - // For balanced pools, the swap ratios should be approximately symmetric int128 ratio0to1 = amountOut0to1.div(amountIn0to1); int128 ratio1to0 = amountOut1to0.div(amountIn1to0); @@ -814,9 +789,6 @@ contract LMSRStabilizedTest is Test { int128 ratioDifference = (ratio0to1.sub(ratio1to0)).abs(); int128 relativeRatioDiff = ratioDifference.div(ratio0to1.add(ratio1to0).div(ABDKMath64x64.fromInt(2))); - console2.log("Relative ratio difference (micro-units):"); - console2.logInt(_toMicro(relativeRatioDiff)); - // Assert that the relative difference between ratios is small int128 tolerance = ABDKMath64x64.divu(5, 100); // 5% tolerance assertLt(relativeRatioDiff, tolerance, diff --git a/test/PartyPool.t.sol b/test/PartyPool.t.sol index a2159a7..82f1895 100644 --- a/test/PartyPool.t.sol +++ b/test/PartyPool.t.sol @@ -2,7 +2,6 @@ /* solhint-disable */ pragma solidity ^0.8.30; -import "forge-std/console2.sol"; import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol"; import {CommonBase} from "../lib/forge-std/src/Base.sol"; import {StdAssertions} from "../lib/forge-std/src/StdAssertions.sol"; @@ -254,14 +253,15 @@ contract PartyPoolTest is Test { /// does not undercharge (no value extraction). This test verifies the request succeeds /// and that computed deposits are at least the proportional floor (ceil >= floor). function testProportionalMintOneWeiSucceedsAndProtectsPool() public { - // Request a tiny LP amount (1 wei). Approve pool to transfer _tokens on alice's behalf. + // Request a tiny LP amount. Approve pool to transfer _tokens on alice's behalf. vm.startPrank(alice); token0.approve(address(pool), type(uint256).max); token1.approve(address(pool), type(uint256).max); token2.approve(address(pool), type(uint256).max); // Inspect the deposit amounts that the pool will require (these are rounded up) - uint256[] memory deposits = info.mintAmounts(pool, 1); + uint256 lpAmount = pool.totalSupply() / 2**64 + 1; // smallest mintable amount + uint256[] memory deposits = info.mintAmounts(pool, lpAmount); // Basic sanity: deposits array length must match token count and not all zero necessarily assertEq(deposits.length, 3); @@ -270,16 +270,16 @@ contract PartyPoolTest is Test { uint256 totalLp = pool.totalSupply(); for (uint i = 0; i < deposits.length; i++) { uint256 bal = IERC20(pool.allTokens()[i]).balanceOf(address(pool)); - uint256 floorProportional = (1 * bal) / totalLp; // floor + uint256 floorProportional = (lpAmount * bal) / totalLp; // floor // Ceil (deposit) must be >= floor (pool protected) assertTrue(deposits[i] >= floorProportional, "deposit must not be less than floor proportion"); } // Perform the mint — it should succeed for a 1 wei request (pool uses ceil to protect itself) - pool.mint(alice, alice, 1, 0); + pool.mint(alice, alice, lpAmount, 0); // After mint, alice should have received at least 1 wei of LP - assertTrue(pool.balanceOf(alice) >= 1, "Alice should receive at least 1 wei LP"); + assertTrue(pool.balanceOf(alice) >= lpAmount, "Alice should receive more LP token"); vm.stopPrank(); } @@ -301,9 +301,10 @@ contract PartyPoolTest is Test { poolValueBefore += IERC20(toks[i]).balanceOf(address(pool)); } uint256 totalLpBefore = pool.totalSupply(); + uint256 lpAmount = totalLpBefore/10**18; // tiny amount // Compute required deposits and perform mint for 1 wei - uint256[] memory deposits = info.mintAmounts(pool, 1); + uint256[] memory deposits = info.mintAmounts(pool, lpAmount); // Sum deposits as deposited_value uint256 depositedValue = 0; @@ -311,8 +312,8 @@ contract PartyPoolTest is Test { depositedValue += deposits[i]; } - // Execute mint; it may revert if actualLpToMint == 0 but for 1 wei we expect it to succeed per design. - pool.mint(alice, alice, 1, 0); + // Execute mint; it may revert if actualLpToMint == 0 but for small nonzero values we expect it to succeed per design. + pool.mint(alice, alice, lpAmount, 0); // Observe minted LP uint256 totalLpAfter = pool.totalSupply(); @@ -321,6 +322,7 @@ contract PartyPoolTest is Test { require(minted > 0, "sanity: minted should be > 0 for this test"); // Economic invariant check: + // The depositor should pay at least as much value per LP token as the pool's rate before the mint: // depositedValue / minted >= poolValueBefore / totalLpBefore // Rearranged (to avoid fractional math): depositedValue * totalLpBefore >= poolValueBefore * minted // Use >= to allow the pool to charge equal-or-more value per LP (protects against extraction). @@ -1079,8 +1081,12 @@ contract PartyPoolTest is Test { // 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"); + // Allow a small tolerance for fixed-point rounding (~1e-9) + int128 ratio = ABDKMath64x64.div(price, expected); + int128 expectedRatio = ABDKMath64x64.fromUInt(1); + int128 tol = ABDKMath64x64.divu(1, 1_000_000_000); + int128 diff = ratio.sub(expectedRatio).abs(); + assertLe(diff, tol, "poolPrice(token0) should be ~ 1.000000000"); // Mint a small amount of LP into the pool from alice and verify price remains 1.0 vm.startPrank(alice); @@ -1105,7 +1111,57 @@ contract PartyPoolTest is Test { // Re-query the pool price and ensure it remains 1.0 (within exact fixed-point equality) int128 priceAfter = info.poolPrice(pool, 0); - assertEq(uint256(uint128(priceAfter)), uint256(uint128(expected)), "Pool price should remain 1.0000000 after mint"); + // Allow a small tolerance for fixed-point rounding (~1e-9) + ratio = ABDKMath64x64.div(price, priceAfter); + expectedRatio = ABDKMath64x64.fromUInt(1); + tol = ABDKMath64x64.divu(1, 1_000_000_000); + diff = ratio.sub(expectedRatio).abs(); + assertLe(diff, tol, "Pool price should remain 1.0000000 after mint"); + } + + /// @notice For the same 3x-imbalanced pool, verify that the LP pool price in terms of + /// token0 is 1/3 of the pool price in terms of token1 (up to rounding). + function testPoolPriceWhenToken0HasThreeTimesToken1() public { + // Build tokens array (reuse test tokens) + IERC20[] memory tokens = new IERC20[](3); + tokens[0] = IERC20(address(token0)); + tokens[1] = IERC20(address(token1)); + tokens[2] = IERC20(address(token2)); + + uint256 feePpm = 1000; + int128 kappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); + + // Same 3x imbalance as in testPriceWhenToken0HasThreeTimesToken1 + uint256[] memory deposits = new uint256[](3); + deposits[0] = INIT_BAL * 3; // token0 = 3 * INIT_BAL + deposits[1] = INIT_BAL; // token1 = INIT_BAL + deposits[2] = INIT_BAL * 2; // token2 = 2 * INIT_BAL + + (IPartyPool poolCustom, ) = Deploy.newPartyPoolWithDeposits( + "LP3X_POOLPRICE", + "LP3X_POOLPRICE", + tokens, + kappa, + feePpm, + feePpm, + false, + deposits, + INIT_BAL * 6 * 10**18 + ); + + // Get LP price in terms of token0 and token1 (Q64.64, quote units per LP) + int128 p0 = info.poolPrice(poolCustom, 0); // token0 as quote + int128 p1 = info.poolPrice(poolCustom, 1); // token1 as quote + + // ratio = p0 / p1 should be close to 3 + int128 ratio = ABDKMath64x64.div(p0, p1); + int128 expectedRatio = ABDKMath64x64.fromUInt(3); + + // Allow a small tolerance for fixed-point rounding (~1e-6) + int128 tol = ABDKMath64x64.divu(1, 1_000_000); + int128 diff = ratio.sub(expectedRatio).abs(); + + assertLe(diff, tol, "poolPrice(token0) should be ~ 1/3 of poolPrice(token1)"); } /// @notice Create a 3-token pool where token0 has 3x the balance of token1 and verify @@ -1183,12 +1239,9 @@ contract PartyPoolTest is Test { // Compute swap-implied price as Q64.64 (quote per base) = amountOut / netIn int128 swapPrice = ABDKMath64x64.divu(amountOut, netIn); - console2.log('info price', infoPrice); - console2.log('swap price', swapPrice); // Absolute difference between info.price and swap-implied price int128 slippage = ABDKMath64x64.fromUInt(1) - swapPrice.div(infoPrice); - console2.log('slippage', slippage); // Tolerance ~ 4e-5 in Q64.64 int128 tol = ABDKMath64x64.divu(4, 100_000);