From 28b947436398495eff9c3842c27df9c89d344ebf Mon Sep 17 00:00:00 2001 From: tim Date: Fri, 26 Sep 2025 11:48:01 -0400 Subject: [PATCH] PartyPoolMintImpl --- foundry.toml | 2 +- src/Deploy.sol | 7 +- src/PartyPlanner.sol | 14 ++- src/PartyPool.sol | 160 ++++++---------------------- src/PartyPoolMintImpl.sol | 215 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 264 insertions(+), 134 deletions(-) create mode 100644 src/PartyPoolMintImpl.sol diff --git a/foundry.toml b/foundry.toml index 34623cf..33c6886 100644 --- a/foundry.toml +++ b/foundry.toml @@ -9,7 +9,7 @@ remappings = [ optimizer=true optimizer_runs=999999999 viaIR=true -gas_reports = ['PartyPool', 'PartyPlanner', 'PartyPoolSwapMintImpl', 'PartyPoolViewImpl'] +gas_reports = ['PartyPool', 'PartyPlanner', 'PartyPoolSwapMintImpl', 'PartyPoolMintImpl',] fs_permissions = [{ access = "write", path = "chain.json"}] [lint] diff --git a/src/Deploy.sol b/src/Deploy.sol index 61f674a..2474326 100644 --- a/src/Deploy.sol +++ b/src/Deploy.sol @@ -4,13 +4,15 @@ pragma solidity ^0.8.30; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {PartyPool} from "./PartyPool.sol"; import {PartyPoolSwapMintImpl} from "./PartyPoolSwapMintImpl.sol"; +import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol"; import {PartyPlanner} from "./PartyPlanner.sol"; library Deploy { function newPartyPlanner() internal returns (PartyPlanner) { return new PartyPlanner( - new PartyPoolSwapMintImpl() + new PartyPoolSwapMintImpl(), + new PartyPoolMintImpl() ); } @@ -25,7 +27,8 @@ library Deploy { bool _stable ) internal returns (PartyPool) { return new PartyPool(name_, symbol_, tokens_, bases_, _kappa, _swapFeePpm, _flashFeePpm, _stable, - new PartyPoolSwapMintImpl() + new PartyPoolSwapMintImpl(), + address(new PartyPoolMintImpl()) ); } diff --git a/src/PartyPlanner.sol b/src/PartyPlanner.sol index 442c33c..d873dd8 100644 --- a/src/PartyPlanner.sol +++ b/src/PartyPlanner.sol @@ -7,6 +7,7 @@ import "./LMSRStabilized.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {PartyPoolSwapMintImpl} from "./PartyPoolSwapMintImpl.sol"; +import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol"; /// @title PartyPlanner /// @notice Factory contract for creating and tracking PartyPool instances @@ -17,6 +18,9 @@ contract PartyPlanner is IPartyPlanner { /// @notice Address of the SwapMint implementation contract used by all pools created by this factory address public immutable swapMintImpl; + /// @notice Address of the Mint implementation contract used by all pools created by this factory + address public immutable mintImpl; + // On-chain pool indexing PartyPool[] private _allPools; IERC20[] private _allTokens; @@ -25,9 +29,12 @@ contract PartyPlanner is IPartyPlanner { mapping(IERC20 => PartyPool[]) private _poolsByToken; /// @param _swapMintImpl address of the SwapMint implementation contract to be used by all pools - constructor(PartyPoolSwapMintImpl _swapMintImpl) { - require(address(_swapMintImpl) != address(0), "Planner: impl address cannot be zero"); + /// @param _mintImpl address of the Mint implementation contract to be used by all pools + constructor(PartyPoolSwapMintImpl _swapMintImpl, PartyPoolMintImpl _mintImpl) { + require(address(_swapMintImpl) != address(0), "Planner: swapMintImpl address cannot be zero"); swapMintImpl = address(_swapMintImpl); + require(address(_mintImpl) != address(0), "Planner: mintImpl address cannot be zero"); + mintImpl = address(_mintImpl); } /// Main newPool variant: accepts kappa directly (preferred). @@ -67,7 +74,8 @@ contract PartyPlanner is IPartyPlanner { _swapFeePpm, _flashFeePpm, _stable, - PartyPoolSwapMintImpl(swapMintImpl) + PartyPoolSwapMintImpl(swapMintImpl), + mintImpl ); _allPools.push(pool); diff --git a/src/PartyPool.sol b/src/PartyPool.sol index 3494d06..0c7d101 100644 --- a/src/PartyPool.sol +++ b/src/PartyPool.sol @@ -48,6 +48,9 @@ contract PartyPool is PartyPoolBase, IPartyPool { /// @notice Address of the SwapMint implementation contract for delegatecall address public immutable swapMintImpl; + /// @notice Address of the Mint implementation contract for delegatecall + address public immutable mintImpl; + /// @inheritdoc IPartyPool function getToken(uint256 i) external view returns (IERC20) { return tokens[i]; } @@ -69,6 +72,7 @@ contract PartyPool is PartyPoolBase, IPartyPool { /// @param _flashFeePpm fee in parts-per-million, taken for flash loans /// @param _stable if true and assets.length==2, then the optimization for 2-asset stablecoin pools is activated. /// @param _swapMintImpl address of the SwapMint implementation contract + /// @param _mintImpl address of the Mint implementation contract constructor( string memory name_, string memory symbol_, @@ -78,7 +82,8 @@ contract PartyPool is PartyPoolBase, IPartyPool { uint256 _swapFeePpm, uint256 _flashFeePpm, bool _stable, - PartyPoolSwapMintImpl _swapMintImpl + PartyPoolSwapMintImpl _swapMintImpl, + address _mintImpl ) PartyPoolBase(name_, symbol_) { require(tokens_.length > 1, "Pool: need >1 asset"); require(tokens_.length == bases_.length, "Pool: lengths mismatch"); @@ -90,8 +95,10 @@ contract PartyPool is PartyPoolBase, IPartyPool { require(_flashFeePpm < 1_000_000, "Pool: flash fee >= ppm"); flashFeePpm = _flashFeePpm; _stablePair = _stable && tokens_.length == 2; - require(address(_swapMintImpl) != address(0), "Pool: impl address zero"); + require(address(_swapMintImpl) != address(0), "Pool: swapMintImpl address zero"); swapMintImpl = address(_swapMintImpl); + require(_mintImpl != address(0), "Pool: mintImpl address zero"); + mintImpl = _mintImpl; uint256 n = tokens_.length; @@ -115,6 +122,10 @@ contract PartyPool is PartyPoolBase, IPartyPool { /// @inheritdoc IPartyPool function mintDepositAmounts(uint256 lpTokenAmount) public view returns (uint256[] memory depositAmounts) { + return _mintDepositAmounts(lpTokenAmount); + } + + function _mintDepositAmounts(uint256 lpTokenAmount) internal view returns (uint256[] memory depositAmounts) { uint256 n = tokens.length; depositAmounts = new uint256[](n); @@ -180,77 +191,23 @@ contract PartyPool is PartyPoolBase, IPartyPool { } /// @notice Proportional mint for existing pool. - /// @dev Payer must approve the required token amounts before calling. - /// Can only be called when pool is already initialized (totalSupply() > 0 and lmsr.nAssets > 0). - /// Rounds follow the pool-favorable conventions documented in helpers (ceil inputs, floor outputs). + /// @dev This function forwards the call to the mint implementation via delegatecall /// @param payer address that provides the input tokens /// @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 returns (uint256 lpMinted) { - require(deadline == 0 || block.timestamp <= deadline, "mint: deadline exceeded"); - uint256 n = tokens.length; + bytes memory data = abi.encodeWithSignature( + "mint(address,address,uint256,uint256)", + payer, + receiver, + lpTokenAmount, + deadline + ); - // Check if this is NOT initial deposit - revert if it is - bool isInitialDeposit = totalSupply() == 0 || lmsr.nAssets == 0; - require(!isInitialDeposit, "mint: use initialMint for pool initialization"); - require(lpTokenAmount > 0, "mint: zero LP amount"); - - // Capture old pool size metric (scaled) by computing from current balances - int128 oldTotal = _computeSizeMetric(lmsr.qInternal); - uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE); - - // Calculate required deposit amounts for the desired LP tokens - uint256[] memory depositAmounts = mintDepositAmounts(lpTokenAmount); - - // Transfer in all token amounts - for (uint i = 0; i < n; ) { - if (depositAmounts[i] > 0) { - tokens[i].safeTransferFrom(payer, address(this), depositAmounts[i]); - } - unchecked { i++; } - } - - // Update cached balances for all assets - int128[] memory newQInternal = new int128[](n); - for (uint i = 0; i < n; ) { - uint256 bal = IERC20(tokens[i]).balanceOf(address(this)); - cachedUintBalances[i] = bal; - newQInternal[i] = _uintToInternalFloor(bal, bases[i]); - unchecked { i++; } - } - - // Update for proportional change - lmsr.updateForProportionalChange(newQInternal); - - // Compute actual LP tokens to mint based on change in size metric (scaled) - // floor truncation rounds in favor of the pool - int128 newTotal = _computeSizeMetric(newQInternal); - uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE); - uint256 actualLpToMint; - - require(oldScaled > 0, "mint: oldScaled zero"); - uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0; - // Proportional issuance: totalSupply * delta / oldScaled - if (delta > 0) { - // floor truncation rounds in favor of the pool - actualLpToMint = (totalSupply() * delta) / oldScaled; - } else { - actualLpToMint = 0; - } - - // 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 Mint(payer, receiver, depositAmounts, actualLpToMint); - return actualLpToMint; + bytes memory result = Address.functionDelegateCall(mintImpl, data); + return abi.decode(result, (uint256)); } /// @inheritdoc IPartyPool @@ -280,74 +237,21 @@ contract PartyPool is PartyPoolBase, IPartyPool { } /// @notice Burn LP tokens and withdraw the proportional basket to receiver. - /// @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. + /// @dev This function forwards the call to the burn implementation via delegatecall /// @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. function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external nonReentrant { - require(deadline == 0 || block.timestamp <= deadline, "burn: deadline exceeded"); - uint256 n = tokens.length; - require(lpAmount > 0, "burn: zero lp"); + bytes memory data = abi.encodeWithSignature( + "burn(address,address,uint256,uint256)", + payer, + receiver, + lpAmount, + deadline + ); - uint256 supply = totalSupply(); - require(supply > 0, "burn: empty supply"); - require(lmsr.nAssets > 0, "burn: uninit pool"); - require(balanceOf(payer) >= lpAmount, "burn: insufficient LP"); - - // Refresh cached balances to reflect current on-chain balances before computing withdrawal amounts - for (uint i = 0; i < n; ) { - uint256 bal = IERC20(tokens[i]).balanceOf(address(this)); - cachedUintBalances[i] = bal; - unchecked { i++; } - } - - // Compute proportional withdrawal amounts for the requested LP amount (rounded down) - uint256[] memory withdrawAmounts = _burnReceiveAmounts(lpAmount); - - // Transfer underlying tokens out to receiver according to computed proportions - for (uint i = 0; i < n; ) { - if (withdrawAmounts[i] > 0) { - tokens[i].safeTransfer(receiver, withdrawAmounts[i]); - } - unchecked { i++; } - } - - // Update cached balances and internal q for all assets - int128[] memory newQInternal = new int128[](n); - for (uint i = 0; i < n; ) { - uint256 bal = IERC20(tokens[i]).balanceOf(address(this)); - cachedUintBalances[i] = bal; - newQInternal[i] = _uintToInternalFloor(bal, bases[i]); - 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 { - lmsr.updateForProportionalChange(newQInternal); - } - - // Burn exactly the requested LP amount from payer (authorization via allowance) - if (msg.sender != payer) { - uint256 allowed = allowance(payer, msg.sender); - require(allowed >= lpAmount, "burn: allowance insufficient"); - _approve(payer, msg.sender, allowed - lpAmount); - } - _burn(payer, lpAmount); - - emit Burn(payer, receiver, withdrawAmounts, lpAmount); + Address.functionDelegateCall(mintImpl, data); } /* ---------------------- diff --git a/src/PartyPoolMintImpl.sol b/src/PartyPoolMintImpl.sol new file mode 100644 index 0000000..cef3a62 --- /dev/null +++ b/src/PartyPoolMintImpl.sol @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.30; + +import "@abdk/ABDKMath64x64.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "./PartyPoolBase.sol"; +import "./LMSRStabilized.sol"; + +/// @title PartyPoolMintImpl - Implementation contract for mint and burn functions +/// @notice This contract contains the mint and burn implementation that will be called via delegatecall +/// @dev This contract inherits from PartyPoolBase to access storage and internal functions +contract PartyPoolMintImpl is PartyPoolBase { + using ABDKMath64x64 for int128; + using LMSRStabilized for LMSRStabilized.State; + using SafeERC20 for IERC20; + + // Events that mirror the main contract events + event Mint(address indexed payer, address indexed receiver, uint256[] depositAmounts, uint256 lpMinted); + event Burn(address indexed payer, address indexed receiver, uint256[] withdrawAmounts, uint256 lpBurned); + + constructor() PartyPoolBase('','') {} + + /// @notice Proportional mint for existing pool. + /// @dev Payer must approve the required token amounts before calling. + /// Can only be called when pool is already initialized (totalSupply() > 0 and lmsr.nAssets > 0). + /// Rounds follow the pool-favorable conventions documented in helpers (ceil inputs, floor outputs). + /// @param payer address that provides the input tokens + /// @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 returns (uint256 lpMinted) { + require(deadline == 0 || block.timestamp <= deadline, "mint: deadline exceeded"); + uint256 n = tokens.length; + + // Check if this is NOT initial deposit - revert if it is + bool isInitialDeposit = totalSupply() == 0 || lmsr.nAssets == 0; + require(!isInitialDeposit, "mint: use initialMint for pool initialization"); + require(lpTokenAmount > 0, "mint: zero LP amount"); + + // Capture old pool size metric (scaled) by computing from current balances + int128 oldTotal = _computeSizeMetric(lmsr.qInternal); + uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE); + + // Calculate required deposit amounts for the desired LP tokens + uint256[] memory depositAmounts = _mintDepositAmounts(lpTokenAmount); + + // Transfer in all token amounts + for (uint i = 0; i < n; ) { + if (depositAmounts[i] > 0) { + tokens[i].safeTransferFrom(payer, address(this), depositAmounts[i]); + } + unchecked { i++; } + } + + // Update cached balances for all assets + int128[] memory newQInternal = new int128[](n); + for (uint i = 0; i < n; ) { + uint256 bal = IERC20(tokens[i]).balanceOf(address(this)); + cachedUintBalances[i] = bal; + newQInternal[i] = _uintToInternalFloor(bal, bases[i]); + unchecked { i++; } + } + + // Update for proportional change + lmsr.updateForProportionalChange(newQInternal); + + // Compute actual LP tokens to mint based on change in size metric (scaled) + // floor truncation rounds in favor of the pool + int128 newTotal = _computeSizeMetric(newQInternal); + uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE); + uint256 actualLpToMint; + + require(oldScaled > 0, "mint: oldScaled zero"); + uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0; + // Proportional issuance: totalSupply * delta / oldScaled + if (delta > 0) { + // floor truncation rounds in favor of the pool + actualLpToMint = (totalSupply() * delta) / oldScaled; + } else { + actualLpToMint = 0; + } + + // 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 Mint(payer, receiver, depositAmounts, actualLpToMint); + return actualLpToMint; + } + + /// @notice Burn LP tokens and withdraw the proportional basket to receiver. + /// @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 deadline timestamp after which the transaction will revert. Pass 0 to ignore. + function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external { + require(deadline == 0 || block.timestamp <= deadline, "burn: deadline exceeded"); + uint256 n = tokens.length; + require(lpAmount > 0, "burn: zero lp"); + + uint256 supply = totalSupply(); + require(supply > 0, "burn: empty supply"); + require(lmsr.nAssets > 0, "burn: uninit pool"); + require(balanceOf(payer) >= lpAmount, "burn: insufficient LP"); + + // Refresh cached balances to reflect current on-chain balances before computing withdrawal amounts + for (uint i = 0; i < n; ) { + uint256 bal = IERC20(tokens[i]).balanceOf(address(this)); + cachedUintBalances[i] = bal; + unchecked { i++; } + } + + // Compute proportional withdrawal amounts for the requested LP amount (rounded down) + uint256[] memory withdrawAmounts = _burnReceiveAmounts(lpAmount); + + // Transfer underlying tokens out to receiver according to computed proportions + for (uint i = 0; i < n; ) { + if (withdrawAmounts[i] > 0) { + tokens[i].safeTransfer(receiver, withdrawAmounts[i]); + } + unchecked { i++; } + } + + // Update cached balances and internal q for all assets + int128[] memory newQInternal = new int128[](n); + for (uint i = 0; i < n; ) { + uint256 bal = IERC20(tokens[i]).balanceOf(address(this)); + cachedUintBalances[i] = bal; + newQInternal[i] = _uintToInternalFloor(bal, bases[i]); + 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 { + lmsr.updateForProportionalChange(newQInternal); + } + + // Burn exactly the requested LP amount from payer (authorization via allowance) + if (msg.sender != payer) { + uint256 allowed = allowance(payer, msg.sender); + require(allowed >= lpAmount, "burn: allowance insufficient"); + _approve(payer, msg.sender, allowed - lpAmount); + } + _burn(payer, lpAmount); + + emit Burn(payer, receiver, withdrawAmounts, lpAmount); + } + + /// @notice Internal helper to calculate required deposit amounts for minting LP tokens + function _mintDepositAmounts(uint256 lpTokenAmount) internal view returns (uint256[] memory depositAmounts) { + uint256 n = tokens.length; + depositAmounts = new uint256[](n); + + // If this is the first mint or pool is empty, return zeros + // For first mint, tokens should already be transferred to the pool + if (totalSupply() == 0 || lmsr.nAssets == 0) { + return depositAmounts; // Return zeros, initial deposit handled differently + } + + // Calculate deposit based on current proportions + uint256 totalLpSupply = totalSupply(); + + // lpTokenAmount / totalLpSupply = depositAmount / currentBalance + // Therefore: depositAmount = (lpTokenAmount * currentBalance) / totalLpSupply + // We round up to protect the pool + for (uint i = 0; i < n; i++) { + uint256 currentBalance = cachedUintBalances[i]; + // Calculate with rounding up: (a * b + c - 1) / c + depositAmounts[i] = (lpTokenAmount * currentBalance + totalLpSupply - 1) / totalLpSupply; + } + + return depositAmounts; + } + + /// @notice Internal helper to calculate withdrawal amounts for burning LP tokens + function _burnReceiveAmounts(uint256 lpTokenAmount) internal view returns (uint256[] memory withdrawAmounts) { + uint256 n = tokens.length; + withdrawAmounts = new uint256[](n); + + // If supply is zero or pool uninitialized, return zeros + if (totalSupply() == 0 || lmsr.nAssets == 0) { + return withdrawAmounts; // Return zeros, nothing to withdraw + } + + // Calculate withdrawal amounts based on current proportions + uint256 totalLpSupply = totalSupply(); + + // withdrawAmount = floor(lpTokenAmount * currentBalance / totalLpSupply) + for (uint i = 0; i < n; i++) { + uint256 currentBalance = cachedUintBalances[i]; + withdrawAmounts[i] = (lpTokenAmount * currentBalance) / totalLpSupply; + } + + return withdrawAmounts; + } +}