From 6795886eabaf7a7834f86f209ef6a39ee94c267b Mon Sep 17 00:00:00 2001 From: tim Date: Mon, 1 Dec 2025 16:26:58 -0400 Subject: [PATCH] mint & burn streamlining --- src/PartyInfo.sol | 2 +- src/PartyPoolBase.sol | 4 --- src/PartyPoolHelpers.sol | 4 +++ src/PartyPoolMintImpl.sol | 62 ++++++++++++++++----------------------- test/GasTest.sol | 2 +- test/PartyPool.t.sol | 8 ++--- 6 files changed, 35 insertions(+), 47 deletions(-) diff --git a/src/PartyInfo.sol b/src/PartyInfo.sol index 135065a..cdbb45b 100644 --- a/src/PartyInfo.sol +++ b/src/PartyInfo.sol @@ -72,7 +72,7 @@ contract PartyInfo is PartyPoolHelpers, IPartyInfo { uint256 qd = pool.denominators()[quoteTokenIndex]; uint256 supply = pool.totalSupply(); - return value.mul(ABDKMath64x64.divu(qd * 10**18, supply)); + return value.mul(ABDKMath64x64.divu(qd * LP_SCALE, supply)); } diff --git a/src/PartyPoolBase.sol b/src/PartyPoolBase.sol index 1c0b474..8c238ee 100644 --- a/src/PartyPoolBase.sol +++ b/src/PartyPoolBase.sol @@ -38,10 +38,6 @@ abstract contract PartyPoolBase is OwnableInternal, ERC20Internal, ReentrancyGua // LMSR internal state LMSRStabilized.State internal _lmsr; - /// @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 internal constant LP_SCALE = 1e18; // Scale used to convert LMSR lastTotal (Q64.64) into LP token units (uint) - /// @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. IERC20[] internal _tokens; // effectively immutable since there is no interface to change the _tokens diff --git a/src/PartyPoolHelpers.sol b/src/PartyPoolHelpers.sol index 337ec1c..cf09a10 100644 --- a/src/PartyPoolHelpers.sol +++ b/src/PartyPoolHelpers.sol @@ -6,6 +6,10 @@ import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol"; abstract contract PartyPoolHelpers { using ABDKMath64x64 for int128; + /// @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 internal constant LP_SCALE = 1e18; // Scale used to convert LMSR lastTotal (Q64.64) into LP token units (uint) + /// @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) { diff --git a/src/PartyPoolMintImpl.sol b/src/PartyPoolMintImpl.sol index c1ce467..b09253f 100644 --- a/src/PartyPoolMintImpl.sol +++ b/src/PartyPoolMintImpl.sol @@ -91,19 +91,15 @@ contract PartyPoolMintImpl is PartyPoolBase { uint256[] memory depositAmounts = mintAmounts(lpTokenAmount, _totalSupply, _cachedUintBalances); // Transfer in all token amounts - for (uint i = 0; i < n; ) { - if (depositAmounts[i] > 0) { - _receiveTokenFrom(payer, _tokens[i], depositAmounts[i]); - } - unchecked { i++; } - } - - // Update cached balances and internal q for all assets using depositAmounts int128[] memory newQInternal = new int128[](n); for (uint i = 0; i < n; ) { - uint256 newBal = _cachedUintBalances[i] + depositAmounts[i]; - _cachedUintBalances[i] = newBal; - newQInternal[i] = _uintToInternalFloor(newBal, _bases[i]); + uint256 amount = depositAmounts[i]; + if (amount > 0) { + _receiveTokenFrom(payer, _tokens[i], amount); + uint256 newBal = _cachedUintBalances[i] + amount; + _cachedUintBalances[i] = newBal; + newQInternal[i] = _uintToInternalFloor(newBal, _bases[i]); + } unchecked { i++; } } @@ -128,11 +124,6 @@ contract PartyPoolMintImpl is PartyPoolBase { // Ensure the calculated LP amount is not too different from requested require(actualLpToMint > 0, "mint: zero LP minted"); - // Allow actual amount to be at most 0.00001% less than requested - // This accounts for rounding in deposit calculations - uint256 minAcceptable = lpTokenAmount * 99_999 / 100_000; - require(actualLpToMint >= minAcceptable, "mint: insufficient LP minted"); - _mint(receiver, actualLpToMint); emit IPartyPool.Mint(payer, receiver, depositAmounts, actualLpToMint); @@ -162,33 +153,23 @@ contract PartyPoolMintImpl is PartyPoolBase { // Compute proportional withdrawal amounts for the requested LP amount (rounded down) withdrawAmounts = burnAmounts(lpAmount, _totalSupply, _cachedUintBalances); - // Transfer underlying _tokens out to receiver according to computed proportions - for (uint i = 0; i < n; ) { - if (withdrawAmounts[i] > 0) { - _sendTokenTo(_tokens[i], receiver, withdrawAmounts[i], unwrap); - } - unchecked { i++; } - } - // Update cached balances and internal q for all assets using computed withdrawals + bool allZero = true; int128[] memory newQInternal = new int128[](n); for (uint i = 0; i < n; ) { - uint256 newBal = _cachedUintBalances[i] - withdrawAmounts[i]; - _cachedUintBalances[i] = newBal; - newQInternal[i] = _uintToInternalFloor(newBal, _bases[i]); + uint256 amount = withdrawAmounts[i]; + if (amount > 0) { + _sendTokenTo(_tokens[i], receiver, amount, unwrap); + uint256 newBal = _cachedUintBalances[i] - amount; + _cachedUintBalances[i] = newBal; + newQInternal[i] = _uintToInternalFloor(newBal, _bases[i]); + if (newQInternal[i] != int128(0)) + allZero = false; + } unchecked { i++; } } // Apply proportional update or deinitialize if drained - bool allZero = true; - for (uint i = 0; i < n; ) { - if (newQInternal[i] != int128(0)) { - allZero = false; - break; - } - unchecked { i++; } - } - if (allZero) { _lmsr.deinit(); } else { @@ -226,6 +207,7 @@ contract PartyPoolMintImpl is PartyPoolBase { // Compute mint ratio in Q64.64: ratio = lpTokenAmount / totalSupply int128 ratio = ABDKMath64x64.divu(lpTokenAmount, totalSupply); + require(ratio > 0, 'mint too small'); // depositAmount_i = ceil(ratio * currentBalance_i) for (uint i = 0; i < numAssets; i++) { @@ -248,11 +230,17 @@ contract PartyPoolMintImpl is PartyPoolBase { } int128 ratio = ABDKMath64x64.divu(lpTokenAmount, totalSupply); + require(ratio > 0, 'burn too small: tiny input'); + bool nonZero = false; for (uint i = 0; i < numAssets; i++) { uint256 currentBalance = cachedUintBalances[i]; - withdrawAmounts[i] = ratio.mulu(currentBalance); + uint256 amount = ratio.mulu(currentBalance); + withdrawAmounts[i] = amount; + if (amount > 0) + nonZero = true; } + require(nonZero, 'burn too small: no output'); return withdrawAmounts; } diff --git a/test/GasTest.sol b/test/GasTest.sol index d4c9d9a..4a1c25e 100644 --- a/test/GasTest.sol +++ b/test/GasTest.sol @@ -461,7 +461,7 @@ contract GasTest is Test { for (uint256 k = 0; k < iterations; k++) { // Request a tiny LP mint (1 wei) - pool will compute deposits and transfer from alice - uint256 lpRequest = 1; + uint256 lpRequest = testPool.totalSupply() / 10000; // Snapshot alice LP before to compute actual minted uint256 lpBefore = testPool.balanceOf(alice); diff --git a/test/PartyPool.t.sol b/test/PartyPool.t.sol index 82f1895..2c31e62 100644 --- a/test/PartyPool.t.sol +++ b/test/PartyPool.t.sol @@ -458,7 +458,7 @@ contract PartyPoolTest is Test { // Use a range of LP requests (tiny to large fraction) uint256 totalLp = pool.totalSupply(); uint256[] memory requests = new uint256[](4); - requests[0] = 1; + requests[0] = totalLp / 1000; requests[1] = totalLp / 100; // 1% requests[2] = totalLp / 10; // 10% requests[3] = totalLp / 2; // 50% @@ -504,7 +504,7 @@ contract PartyPoolTest is Test { function testMintDepositAmountsMatchesMint_10TokenPool() public { uint256 totalLp = pool10.totalSupply(); uint256[] memory requests = new uint256[](4); - requests[0] = 1; + requests[0] = totalLp / 1000; requests[1] = totalLp / 100; requests[2] = totalLp / 10; requests[3] = totalLp / 2; @@ -568,7 +568,7 @@ contract PartyPoolTest is Test { // Use address(this) as payer (holds initial LP from setUp) uint256 totalLp = pool.totalSupply(); uint256[] memory burns = new uint256[](4); - burns[0] = 1; + burns[0] = totalLp / 1000; burns[1] = totalLp / 100; burns[2] = totalLp / 10; burns[3] = totalLp / 2; @@ -620,7 +620,7 @@ contract PartyPoolTest is Test { function testBurnReceiveAmountsMatchesBurn_10TokenPool() public { uint256 totalLp = pool10.totalSupply(); uint256[] memory burns = new uint256[](4); - burns[0] = 1; + burns[0] = totalLp / 1000; burns[1] = totalLp / 100; burns[2] = totalLp / 10; burns[3] = totalLp / 2;