From 98e80ab6cae3fcd86b1c5583c490586633d6cb43 Mon Sep 17 00:00:00 2001 From: tim Date: Tue, 7 Oct 2025 14:37:44 -0400 Subject: [PATCH] delegated reentrancy lock --- foundry.toml | 2 +- src/PartyPool.sol | 52 +++--------- src/PartyPoolMintImpl.sol | 165 +++++++++++++++++++------------------- src/PartyPoolView.sol | 3 +- test/GasTest.sol | 2 +- test/PartyPool.t.sol | 2 +- 6 files changed, 96 insertions(+), 130 deletions(-) diff --git a/foundry.toml b/foundry.toml index 5950f5d..72bf0e8 100644 --- a/foundry.toml +++ b/foundry.toml @@ -9,7 +9,7 @@ remappings = [ optimizer=true optimizer_runs=999999999 viaIR=true -gas_reports = ['PartyPool', 'PartyPlanner', 'PartyPoolSwapMintImpl', 'PartyPoolMintImpl',] +gas_reports = ['PartyPool', 'PartyPlanner', 'PartyPoolSwapImpl', 'PartyPoolMintImpl',] fs_permissions = [{ access = "write", path = "chain.json"}] [lint] diff --git a/src/PartyPool.sol b/src/PartyPool.sol index 34b79cb..39ec08a 100644 --- a/src/PartyPool.sol +++ b/src/PartyPool.sol @@ -144,7 +144,7 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { ---------------------- */ /// @inheritdoc IPartyPool - function initialMint(address receiver, uint256 lpTokens) external nonReentrant + function initialMint(address receiver, uint256 lpTokens) external returns (uint256 lpMinted) { bytes memory data = abi.encodeWithSignature( "initialMint(address,uint256,int128)", @@ -162,7 +162,7 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { /// @param receiver address that receives the LP tokens /// @param lpTokenAmount desired amount of LP tokens to mint /// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore. - function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external nonReentrant + function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external returns (uint256 lpMinted) { bytes memory data = abi.encodeWithSignature( "mint(address,address,uint256,uint256)", @@ -181,7 +181,7 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { /// @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. - function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external nonReentrant + function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external returns (uint256[] memory withdrawAmounts) { bytes memory data = abi.encodeWithSignature( "burn(address,address,uint256,uint256)", @@ -220,31 +220,20 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { int128 limitPrice, uint256 deadline ) external nonReentrant returns (uint256 amountIn, uint256 amountOut, uint256 fee) { - uint256 n = tokens.length; - require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx"); - require(maxAmountIn > 0, "swap: input zero"); require(deadline == 0 || block.timestamp <= deadline, "swap: deadline exceeded"); - // Read previous balances for affected assets - uint256 prevBalI = IERC20(tokens[inputTokenIndex]).balanceOf(address(this)); - uint256 prevBalJ = IERC20(tokens[outputTokenIndex]).balanceOf(address(this)); - // Compute amounts using the same path as views (uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalUsed, int128 amountOutInternal, , uint256 feeUint) = _quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice); - // Transfer the exact amount from payer and require exact receipt (revert on fee-on-transfer) + // Transfer tokens tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount); uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this)); - require(balIAfter == prevBalI + totalTransferAmount, "swap: non-standard tokenIn"); - - // Transfer output to receiver and verify exact decrease tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint); uint256 balJAfter = IERC20(tokens[outputTokenIndex]).balanceOf(address(this)); - require(balJAfter == prevBalJ - amountOutUint, "swap: non-standard tokenOut"); // Accrue protocol share (floor) from the fee on input token - if (PROTOCOL_FEE_PPM > 0 && feeUint > 0 && PROTOCOL_FEE_ADDRESS != address(0)) { + if (PROTOCOL_FEE_PPM > 0 && feeUint > 0) { uint256 protoShare = (feeUint * PROTOCOL_FEE_PPM) / 1_000_000; // floor if (protoShare > 0) { protocolFeesOwed[inputTokenIndex] += protoShare; @@ -288,7 +277,6 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { uint256 n = tokens.length; require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx"); require(maxAmountIn > 0, "swap: input zero"); - require(lmsr.nAssets > 0, "swap: empty pool"); // Estimate max net input (fee on gross rounded up, then subtract) (, uint256 netUintForSwap) = _computeFee(maxAmountIn, SWAP_FEE_PPM); @@ -302,7 +290,6 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { // Convert actual used input internal -> uint (ceil) amountInUintNoFee = _internalToUintCeil(amountInInternalUsed, bases[inputTokenIndex]); - require(amountInUintNoFee > 0, "swap: input zero"); // Compute gross transfer including fee on the used input (ceil) feeUint = 0; @@ -317,7 +304,6 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { // Compute output (floor) amountOutUint = _internalToUintFloor(amountOutInternal, bases[outputTokenIndex]); - require(amountOutUint > 0, "swap: output zero"); } @@ -329,7 +315,7 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { uint256 outputTokenIndex, int128 limitPrice, uint256 deadline - ) external nonReentrant returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) { + ) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) { bytes memory data = abi.encodeWithSignature( 'swapToLimit(address,address,uint256,uint256,int128,uint256,uint256,uint256)', payer, @@ -426,18 +412,9 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { IERC20 token = IERC20(tokenAddr); require(amount <= token.balanceOf(address(this))); (uint256 fee, ) = _computeFee(amount, FLASH_FEE_PPM); - require( - token.transfer(address(receiver), amount), - "FlashLender: Transfer failed" - ); - require( - receiver.onFlashLoan(msg.sender, address(token), amount, fee, data) == FLASH_CALLBACK_SUCCESS, - "FlashLender: Callback failed" - ); - require( - token.transferFrom(address(receiver), address(this), amount + fee), - "FlashLender: Repay failed" - ); + require(token.transfer(address(receiver), amount)); + require(receiver.onFlashLoan(msg.sender, address(token), amount, fee, data) == FLASH_CALLBACK_SUCCESS); + require(token.transferFrom(address(receiver), address(this), amount + fee)); return true; } @@ -467,15 +444,4 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { return lmsr.swapAmountsForExactInput(i, j, a, limitPrice); } - /// @notice Compute fee and net amounts for a gross input (fee rounded up to favor the pool). - /// @return feeUint fee taken (uint) and netUint remaining for protocol use (uint) - function _computeFee(uint256 gross) internal view returns (uint256 feeUint, uint256 netUint) { - return _computeFee(gross, SWAP_FEE_PPM); - } - - /// @notice Convenience: return gross = net + fee(net) using ceiling for fee. - function _addFee(uint256 netUint) internal view returns (uint256 gross) { - return _addFee(netUint, SWAP_FEE_PPM); - } - } diff --git a/src/PartyPoolMintImpl.sol b/src/PartyPoolMintImpl.sol index 89ac27e..f73a909 100644 --- a/src/PartyPoolMintImpl.sol +++ b/src/PartyPoolMintImpl.sol @@ -22,7 +22,7 @@ contract PartyPoolMintImpl is PartyPoolBase { // Initialization Mint // - function initialMint(address receiver, uint256 lpTokens, int128 KAPPA) external + function initialMint(address receiver, uint256 lpTokens, int128 KAPPA) external nonReentrant returns (uint256 lpMinted) { uint256 n = tokens.length; @@ -62,7 +62,8 @@ contract PartyPoolMintImpl is PartyPoolBase { // Regular Mint and Burn // - function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external returns (uint256 lpMinted) { + function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external nonReentrant + returns (uint256 lpMinted) { require(deadline == 0 || block.timestamp <= deadline, "mint: deadline exceeded"); uint256 n = tokens.length; @@ -134,7 +135,7 @@ contract PartyPoolMintImpl is PartyPoolBase { /// @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. - function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external + function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external nonReentrant returns (uint256[] memory withdrawAmounts) { require(deadline == 0 || block.timestamp <= deadline, "burn: deadline exceeded"); uint256 n = tokens.length; @@ -252,6 +253,83 @@ contract PartyPoolMintImpl is PartyPoolBase { // Swap-Mint and Burn-Swap // + /// @notice Calculate the amounts for a swap mint operation + /// @dev This is a pure view function that computes swap mint amounts from provided state + /// @param inputTokenIndex index of the input token + /// @param maxAmountIn maximum amount of token to deposit (inclusive of fee) + /// @param swapFeePpm fee in parts-per-million + /// @param lmsrState current LMSR state + /// @param bases_ scaling bases for each token + /// @param totalSupply_ current total LP token supply + /// @return amountInUsed actual input amount used (excluding fee) + /// @return fee fee amount charged + /// @return lpMinted LP tokens that would be minted + function swapMintAmounts( + uint256 inputTokenIndex, + uint256 maxAmountIn, + uint256 swapFeePpm, + LMSRStabilized.State memory lmsrState, + uint256[] memory bases_, + uint256 totalSupply_ + ) public pure returns (uint256 amountInUsed, uint256 fee, uint256 lpMinted) { + require(inputTokenIndex < bases_.length, "swapMintAmounts: idx"); + require(maxAmountIn > 0, "swapMintAmounts: input zero"); + require(lmsrState.nAssets > 0, "swapMintAmounts: uninit pool"); + + // Compute fee on gross maxAmountIn to get an initial net estimate + uint256 feeGuess = 0; + uint256 netUintGuess = maxAmountIn; + if (swapFeePpm > 0) { + feeGuess = (maxAmountIn * swapFeePpm + 999999) / 1000000; // ceil fee + netUintGuess = maxAmountIn - feeGuess; + } + + // Convert the net guess to internal (floor) + int128 netInternalGuess = _uintToInternalFloorPure(netUintGuess, bases_[inputTokenIndex]); + require(netInternalGuess > int128(0), "swapMintAmounts: input too small after fee"); + + // Use LMSR view to determine actual internal consumed and size-increase (ΔS) for mint + (int128 amountInInternalUsed, int128 sizeIncreaseInternal) = + LMSRStabilized.swapAmountsForMint(lmsrState.nAssets, lmsrState.kappa, lmsrState.qInternal, + inputTokenIndex, netInternalGuess); + + // amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer + amountInUsed = _internalToUintCeilPure(amountInInternalUsed, bases_[inputTokenIndex]); + require(amountInUsed > 0, "swapMintAmounts: input zero after internal conversion"); + + // Compute fee on the actual used input (ceiling) + fee = 0; + if (swapFeePpm > 0) { + fee = (amountInUsed * swapFeePpm + 999999) / 1000000; // ceil fee + } + uint256 totalTransfer = amountInUsed + fee; + require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMintAmounts: transfer exceeds max"); + + // Compute old and new scaled size metrics to determine LP minted + int128 oldTotal = _computeSizeMetricPure(lmsrState.qInternal); + require(oldTotal > int128(0), "swapMintAmounts: zero total"); + uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE); + + int128 newTotal = oldTotal.add(sizeIncreaseInternal); + uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE); + + if (totalSupply_ == 0) { + // If somehow supply zero (shouldn't happen as lmsr.nAssets>0), mint newScaled + lpMinted = newScaled; + } else { + require(oldScaled > 0, "swapMintAmounts: oldScaled zero"); + uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0; + if (delta > 0) { + // floor truncation rounds in favor of pool + lpMinted = (totalSupply_ * delta) / oldScaled; + } else { + lpMinted = 0; + } + } + + require(lpMinted > 0, "swapMintAmounts: zero LP minted"); + } + /// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP. /// @dev swapMint executes as an exact-in planned swap followed by proportional scaling of qInternal. /// The function emits SwapMint (gross, net, fee) and also emits Mint for LP issuance. @@ -270,7 +348,7 @@ contract PartyPoolMintImpl is PartyPoolBase { uint256 deadline, uint256 swapFeePpm, uint256 protocolFeePpm - ) external returns (uint256 lpMinted) { + ) external nonReentrant returns (uint256 lpMinted) { uint256 n = tokens.length; require(inputTokenIndex < n, "swapMint: idx"); require(maxAmountIn > 0, "swapMint: input zero"); @@ -362,83 +440,6 @@ contract PartyPoolMintImpl is PartyPoolBase { return actualLpToMint; } - /// @notice Calculate the amounts for a swap mint operation - /// @dev This is a pure view function that computes swap mint amounts from provided state - /// @param inputTokenIndex index of the input token - /// @param maxAmountIn maximum amount of token to deposit (inclusive of fee) - /// @param swapFeePpm fee in parts-per-million - /// @param lmsrState current LMSR state - /// @param bases_ scaling bases for each token - /// @param totalSupply_ current total LP token supply - /// @return amountInUsed actual input amount used (excluding fee) - /// @return fee fee amount charged - /// @return lpMinted LP tokens that would be minted - function swapMintAmounts( - uint256 inputTokenIndex, - uint256 maxAmountIn, - uint256 swapFeePpm, - LMSRStabilized.State memory lmsrState, - uint256[] memory bases_, - uint256 totalSupply_ - ) public pure returns (uint256 amountInUsed, uint256 fee, uint256 lpMinted) { - require(inputTokenIndex < bases_.length, "swapMintAmounts: idx"); - require(maxAmountIn > 0, "swapMintAmounts: input zero"); - require(lmsrState.nAssets > 0, "swapMintAmounts: uninit pool"); - - // Compute fee on gross maxAmountIn to get an initial net estimate - uint256 feeGuess = 0; - uint256 netUintGuess = maxAmountIn; - if (swapFeePpm > 0) { - feeGuess = (maxAmountIn * swapFeePpm + 999999) / 1000000; // ceil fee - netUintGuess = maxAmountIn - feeGuess; - } - - // Convert the net guess to internal (floor) - int128 netInternalGuess = _uintToInternalFloorPure(netUintGuess, bases_[inputTokenIndex]); - require(netInternalGuess > int128(0), "swapMintAmounts: input too small after fee"); - - // Use LMSR view to determine actual internal consumed and size-increase (ΔS) for mint - (int128 amountInInternalUsed, int128 sizeIncreaseInternal) = - LMSRStabilized.swapAmountsForMint(lmsrState.nAssets, lmsrState.kappa, lmsrState.qInternal, - inputTokenIndex, netInternalGuess); - - // amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer - amountInUsed = _internalToUintCeilPure(amountInInternalUsed, bases_[inputTokenIndex]); - require(amountInUsed > 0, "swapMintAmounts: input zero after internal conversion"); - - // Compute fee on the actual used input (ceiling) - fee = 0; - if (swapFeePpm > 0) { - fee = (amountInUsed * swapFeePpm + 999999) / 1000000; // ceil fee - } - uint256 totalTransfer = amountInUsed + fee; - require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMintAmounts: transfer exceeds max"); - - // Compute old and new scaled size metrics to determine LP minted - int128 oldTotal = _computeSizeMetricPure(lmsrState.qInternal); - require(oldTotal > int128(0), "swapMintAmounts: zero total"); - uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE); - - int128 newTotal = oldTotal.add(sizeIncreaseInternal); - uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE); - - if (totalSupply_ == 0) { - // If somehow supply zero (shouldn't happen as lmsr.nAssets>0), mint newScaled - lpMinted = newScaled; - } else { - require(oldScaled > 0, "swapMintAmounts: oldScaled zero"); - uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0; - if (delta > 0) { - // floor truncation rounds in favor of pool - lpMinted = (totalSupply_ * delta) / oldScaled; - } else { - lpMinted = 0; - } - } - - require(lpMinted > 0, "swapMintAmounts: zero LP minted"); - } - /// @notice Calculate the amounts for a burn swap operation /// @dev This is a pure view function that computes burn swap amounts from provided state /// @param lpAmount amount of LP tokens to burn @@ -490,7 +491,7 @@ contract PartyPoolMintImpl is PartyPoolBase { uint256 deadline, uint256 swapFeePpm, uint256 protocolFeePpm - ) external returns (uint256 amountOutUint) { + ) external nonReentrant returns (uint256 amountOutUint) { uint256 n = tokens.length; require(inputTokenIndex < n, "burnSwap: idx"); require(lpAmount > 0, "burnSwap: zero lp"); diff --git a/src/PartyPoolView.sol b/src/PartyPoolView.sol index 3d6998a..89af4c6 100644 --- a/src/PartyPoolView.sol +++ b/src/PartyPoolView.sol @@ -166,13 +166,12 @@ contract PartyPoolView is PartyPoolHelpers { /** * @dev The fee to be charged for a given loan. - * @param token The loan currency. * @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, + address /*token*/, uint256 amount ) external view returns (uint256 fee) { (fee,) = _computeFee(amount, pool.flashFeePpm()); diff --git a/test/GasTest.sol b/test/GasTest.sol index 0a981eb..e851831 100644 --- a/test/GasTest.sol +++ b/test/GasTest.sol @@ -39,7 +39,7 @@ contract FlashBorrower is IERC3156FlashBorrower { } function onFlashLoan( - address initiator, + address /*initiator*/, address token, uint256 amount, uint256 fee, diff --git a/test/PartyPool.t.sol b/test/PartyPool.t.sol index 8aa08e4..da0aaec 100644 --- a/test/PartyPool.t.sol +++ b/test/PartyPool.t.sol @@ -42,7 +42,7 @@ contract FlashBorrower is IERC3156FlashBorrower { } function onFlashLoan( - address initiator, + address /*initiator*/, address token, uint256 amount, uint256 fee,