diff --git a/foundry.toml b/foundry.toml index 9ce8e42..34623cf 100644 --- a/foundry.toml +++ b/foundry.toml @@ -9,7 +9,7 @@ remappings = [ optimizer=true optimizer_runs=999999999 viaIR=true -gas_reports = ['PartyPool', 'PartyPlanner'] +gas_reports = ['PartyPool', 'PartyPlanner', 'PartyPoolSwapMintImpl', 'PartyPoolViewImpl'] fs_permissions = [{ access = "write", path = "chain.json"}] [lint] diff --git a/script/DeployMock.sol b/script/DeployMock.sol index 5caf1f9..64055c5 100644 --- a/script/DeployMock.sol +++ b/script/DeployMock.sol @@ -1,13 +1,14 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.30; +import "../src/Deploy.sol"; +import "../src/IPartyPool.sol"; +import "../src/PartyPlanner.sol"; +import "../src/PartyPool.sol"; +import "../test/MockERC20.sol"; +import "@abdk/ABDKMath64x64.sol"; import "forge-std/Script.sol"; import "forge-std/console2.sol"; -import "@abdk/ABDKMath64x64.sol"; -import "../test/MockERC20.sol"; -import "../src/IPartyPool.sol"; -import "../src/PartyPool.sol"; -import "../src/PartyPlanner.sol"; contract DeployMock is Script { @@ -38,7 +39,7 @@ contract DeployMock is Script { uint256 _feePpm = 100; // deploy a PartyPlanner factory and create the pool via factory - PartyPlanner planner = new PartyPlanner(); + PartyPlanner planner = Deploy.newPartyPlanner(); // prepare initial deposits (10_000 units of each token, scaled by bases) uint256[] memory initialDeposits = new uint256[](3); @@ -56,8 +57,8 @@ contract DeployMock is Script { IERC20(tokens[i]).approve(address(planner), initialDeposits[i]); } - // call full createPool signature on factory which will take the deposits and mint initial LP - (PartyPool pool, uint256 lpAmount) = planner.createPool( + // call full newPool signature on factory which will take the deposits and mint initial LP + (PartyPool pool, ) = planner.newPool( name, symbol, tokens, diff --git a/src/Deploy.sol b/src/Deploy.sol new file mode 100644 index 0000000..61f674a --- /dev/null +++ b/src/Deploy.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.30; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {PartyPool} from "./PartyPool.sol"; +import {PartyPoolSwapMintImpl} from "./PartyPoolSwapMintImpl.sol"; +import {PartyPlanner} from "./PartyPlanner.sol"; + +library Deploy { + + function newPartyPlanner() internal returns (PartyPlanner) { + return new PartyPlanner( + new PartyPoolSwapMintImpl() + ); + } + + function newPartyPool( + string memory name_, + string memory symbol_, + IERC20[] memory tokens_, + uint256[] memory bases_, + int128 _kappa, + uint256 _swapFeePpm, + uint256 _flashFeePpm, + bool _stable + ) internal returns (PartyPool) { + return new PartyPool(name_, symbol_, tokens_, bases_, _kappa, _swapFeePpm, _flashFeePpm, _stable, + new PartyPoolSwapMintImpl() + ); + } + +} diff --git a/src/IPartyPlanner.sol b/src/IPartyPlanner.sol index adc8872..181a1ae 100644 --- a/src/IPartyPlanner.sol +++ b/src/IPartyPlanner.sol @@ -27,7 +27,7 @@ interface IPartyPlanner { /// @param deadline Reverts if nonzero and the current blocktime is later than the deadline /// @return pool Address of the newly created and initialized PartyPool /// @return lpAmount Amount of LP tokens minted to the receiver - function createPool( + function newPool( // Pool constructor args (legacy) string memory name_, string memory symbol_, @@ -61,7 +61,7 @@ interface IPartyPlanner { /// @param deadline Reverts if nonzero and the current blocktime is later than the deadline /// @return pool Address of the newly created and initialized PartyPool /// @return lpAmount Amount of LP tokens minted to the receiver - function createPool( + function newPool( // Pool constructor args (kappa-based) string memory name_, string memory symbol_, @@ -115,4 +115,8 @@ interface IPartyPlanner { /// @param limit Maximum number of items to return /// @return pools Array of pool addresses containing the specified token function getPoolsByToken(IERC20 token, uint256 offset, uint256 limit) external view returns (PartyPool[] memory pools); + + /// @notice Address of the SwapMint implementation contract used by all pools created by this factory + function swapMintImpl() external view returns (address); + } diff --git a/src/IPartyPool.sol b/src/IPartyPool.sol index 868ad29..09647a7 100644 --- a/src/IPartyPool.sol +++ b/src/IPartyPool.sol @@ -55,10 +55,9 @@ interface IPartyPool is IERC20Metadata { ); - // Immutable pool configuration (public getters) /// @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. - function tokens(uint256) external view returns (IERC20); // get single token + function getToken(uint256) external view returns (IERC20); // get single token /// @notice Returns the number of tokens (n) in the pool. function numTokens() external view returns (uint256); @@ -80,10 +79,6 @@ interface IPartyPool is IERC20Metadata { /// @dev Pools are constructed with a κ value; this getter exposes the κ used by the pool. function kappa() external view returns (int128); - /// @notice Mapping from token address => (index+1). A zero value indicates the token is not in the pool. - /// @dev Use index = tokenAddressToIndexPlusOne[token] - 1 when non-zero. - function tokenAddressToIndexPlusOne(IERC20) external view returns (uint); - // Initialization / Mint / Burn (LP token managed) /// @notice Calculate the proportional deposit amounts required for a given LP token amount diff --git a/src/PartyPlanner.sol b/src/PartyPlanner.sol index 80f5fff..442c33c 100644 --- a/src/PartyPlanner.sol +++ b/src/PartyPlanner.sol @@ -6,6 +6,7 @@ import "./PartyPool.sol"; import "./LMSRStabilized.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {PartyPoolSwapMintImpl} from "./PartyPoolSwapMintImpl.sol"; /// @title PartyPlanner /// @notice Factory contract for creating and tracking PartyPool instances @@ -13,6 +14,9 @@ contract PartyPlanner is IPartyPlanner { using SafeERC20 for IERC20; int128 private constant FIXED_ONE_64x64 = int128(1) << 64; + /// @notice Address of the SwapMint implementation contract used by all pools created by this factory + address public immutable swapMintImpl; + // On-chain pool indexing PartyPool[] private _allPools; IERC20[] private _allTokens; @@ -20,8 +24,14 @@ contract PartyPlanner is IPartyPlanner { mapping(IERC20 => bool) private _tokenSupported; mapping(IERC20 => PartyPool[]) private _poolsByToken; - /// Main createPool variant: accepts kappa directly (preferred). - function createPool( + /// @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"); + swapMintImpl = address(_swapMintImpl); + } + + /// Main newPool variant: accepts kappa directly (preferred). + function newPool( // Pool constructor args string memory name_, string memory symbol_, @@ -56,7 +66,8 @@ contract PartyPlanner is IPartyPlanner { _kappa, _swapFeePpm, _flashFeePpm, - _stable + _stable, + PartyPoolSwapMintImpl(swapMintImpl) ); _allPools.push(pool); @@ -89,8 +100,10 @@ contract PartyPlanner is IPartyPlanner { lpAmount = pool.initialMint(receiver, initialLpAmount); } - /// Backwards-compatible convenience overload: compute kappa from (tradeFrac, targetSlippage) then call kappa-based createPool. - function createPool( + // NOTE that the slippage target is only exactly achieved in completely balanced pools where all assets are + // priced the same. This target is actually a minimum slippage that the pool imposes on traders, and the actual + // slippage cost can be multiples bigger in practice due to pool inventory imbalances. + function newPool( // Pool constructor args (old signature) string memory name_, string memory symbol_, @@ -115,8 +128,8 @@ contract PartyPlanner is IPartyPlanner { // Compute kappa from slippage params using LMSR helper (kappa depends only on n, f and s) int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(_tokens.length, _tradeFrac, _targetSlippage); - // Delegate to the kappa-based createPool variant - return createPool( + // Delegate to the kappa-based newPool variant + return newPool( name_, symbol_, _tokens, diff --git a/src/PartyPool.sol b/src/PartyPool.sol index 85d123e..3494d06 100644 --- a/src/PartyPool.sol +++ b/src/PartyPool.sol @@ -3,14 +3,15 @@ pragma solidity ^0.8.30; import "forge-std/console2.sol"; import "@abdk/ABDKMath64x64.sol"; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; import "./LMSRStabilized.sol"; import "./LMSRStabilizedBalancedPair.sol"; import "./IPartyPool.sol"; import "./IPartyFlashCallback.sol"; +import "./PartyPoolBase.sol"; +import {PartyPoolSwapMintImpl} from "./PartyPoolSwapMintImpl.sol"; /// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token /// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model. @@ -25,30 +26,11 @@ import "./IPartyFlashCallback.sol"; /// representation used by the LMSR library. Cached on-chain uint balances are kept to reduce balanceOf calls. /// The contract uses ceiling/floor rules described in function comments to bias rounding in favor of the pool /// (i.e., floor outputs to users, ceil inputs/fees where appropriate). -contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { +contract PartyPool is PartyPoolBase, IPartyPool { using ABDKMath64x64 for int128; using LMSRStabilized for LMSRStabilized.State; using SafeERC20 for IERC20; - - // - // Immutable pool configuration - // - - /// @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[] public tokens; // effectively immutable since there is no interface to change the tokens - - /// @inheritdoc IPartyPool - function numTokens() external view returns (uint256) { return tokens.length; } - - /// @inheritdoc IPartyPool - function allTokens() external view returns (IERC20[] memory) { return tokens; } - - // NOTE that the slippage target is only exactly achieved in completely balanced pools where all assets are - // priced the same. This target is actually a minimum slippage that the pool imposes on traders, and the actual - // slippage cost can be multiples bigger in practice due to pool inventory imbalances. - /// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q) /// @dev Pool is constructed with a fixed κ. Clients that previously passed tradeFrac/targetSlippage /// should use LMSRStabilized.computeKappaFromSlippage(...) to derive κ and pass it here. @@ -60,71 +42,65 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { /// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts. uint256 public immutable flashFeePpm; - // - // Internal state - // - - LMSRStabilized.State internal lmsr; - /// @notice If true and there are exactly two assets, an optimized 2-asset stable-pair path is used for some computations. bool immutable private _stablePair; // if true, the optimized LMSRStabilizedBalancedPair optimization path is enabled - // Cached on-chain balances (uint) and internal 64.64 representation - // balance / base = internal - uint256[] internal cachedUintBalances; + /// @notice Address of the SwapMint implementation contract for delegatecall + address public immutable swapMintImpl; - /// @notice Per-token uint base denominators used to convert uint token amounts <-> internal Q64.64 representation. - /// @dev denominators()[i] is the base for tokens[i]. These bases are chosen by deployer and must match token decimals. - uint256[] internal bases; // per-token uint base used to scale token amounts <-> internal + /// @inheritdoc IPartyPool + function getToken(uint256 i) external view returns (IERC20) { return tokens[i]; } + + /// @inheritdoc IPartyPool + function numTokens() external view returns (uint256) { return tokens.length; } + + /// @inheritdoc IPartyPool + function allTokens() external view returns (IERC20[] memory) { return tokens; } /// @inheritdoc IPartyPool function denominators() external view returns (uint256[] memory) { return bases; } - /// @notice Mapping from token address => (index+1). A zero value indicates the token is not in the pool. - /// @dev Use index = tokenAddressToIndexPlusOne[token] - 1 when non-zero. - mapping(IERC20=>uint) public tokenAddressToIndexPlusOne; // Uses index+1 so a result of 0 indicates a failed lookup - - /// @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 public constant LP_SCALE = 1e18; // Scale used to convert LMSR lastTotal (Q64.64) into LP token units (uint) - /// @param name_ LP token name /// @param symbol_ LP token symbol - /// @param _tokens token addresses (n) - /// @param _bases scaling bases for each token (n) - used when converting to/from internal 64.64 amounts + /// @param tokens_ token addresses (n) + /// @param bases_ scaling bases for each token (n) - used when converting to/from internal 64.64 amounts /// @param _kappa liquidity parameter κ (Q64.64) used to derive b = κ * S(q) /// @param _swapFeePpm fee in parts-per-million, taken from swap input amounts before LMSR calculations /// @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 constructor( string memory name_, string memory symbol_, - IERC20[] memory _tokens, - uint256[] memory _bases, + IERC20[] memory tokens_, + uint256[] memory bases_, int128 _kappa, uint256 _swapFeePpm, uint256 _flashFeePpm, - bool _stable - ) ERC20(name_, symbol_) { - require(_tokens.length > 1, "Pool: need >1 asset"); - require(_tokens.length == _bases.length, "Pool: lengths mismatch"); - tokens = _tokens; - bases = _bases; + bool _stable, + PartyPoolSwapMintImpl _swapMintImpl + ) PartyPoolBase(name_, symbol_) { + require(tokens_.length > 1, "Pool: need >1 asset"); + require(tokens_.length == bases_.length, "Pool: lengths mismatch"); + tokens = tokens_; + bases = bases_; kappa = _kappa; require(_swapFeePpm < 1_000_000, "Pool: fee >= ppm"); swapFeePpm = _swapFeePpm; require(_flashFeePpm < 1_000_000, "Pool: flash fee >= ppm"); flashFeePpm = _flashFeePpm; - _stablePair = _stable && _tokens.length == 2; + _stablePair = _stable && tokens_.length == 2; + require(address(_swapMintImpl) != address(0), "Pool: impl address zero"); + swapMintImpl = address(_swapMintImpl); - uint256 n = _tokens.length; + uint256 n = tokens_.length; // Initialize LMSR state nAssets; full init occurs on first mint when quantities are known. lmsr.nAssets = n; // Initialize token address to index mapping for (uint i = 0; i < n;) { - tokenAddressToIndexPlusOne[_tokens[i]] = i + 1; + tokenAddressToIndexPlusOne[tokens_[i]] = i + 1; unchecked {i++;} } @@ -503,14 +479,6 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { return (amountInUsedUint, amountOutUint, feeUint); } - /// @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) { - if (feePpm == 0) return 0; - // ceil division: (num + denom - 1) / denom - return (x * feePpm + 1_000_000 - 1) / 1_000_000; - } - /// @notice Internal quote for exact-input swap that mirrors swap() rounding and fee application /// @dev Returns amounts consistent with swap() semantics: grossIn includes fees (ceil), amountOut is floored. /// @return grossIn amount to transfer in (inclusive of fee), amountOutUint output amount (uint), @@ -539,7 +507,7 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { require(lmsr.nAssets > 0, "swap: empty pool"); // Estimate max net input (fee on gross rounded up, then subtract) - (, uint256 netUintForSwap) = _computeFee(maxAmountIn); + (, uint256 netUintForSwap) = _computeFee(maxAmountIn, swapFeePpm); // Convert to internal (floor) int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, bases[inputTokenIndex]); @@ -637,9 +605,10 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { // Note: events intentionally avoid exposing internal ΔS and avoid duplicating LP mint/burn data // which is already present in the standard Mint/Burn events. + // todo swapMintAmounts and burnSwapAmounts + /// @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. + /// @dev This function forwards the call to the swapMint implementation via delegatecall /// @param payer who transfers the input token /// @param receiver who receives the minted LP tokens /// @param inputTokenIndex index of the input token @@ -652,95 +621,23 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { uint256 inputTokenIndex, uint256 maxAmountIn, uint256 deadline - ) external nonReentrant returns (uint256 lpMinted) { - uint256 n = tokens.length; - require(inputTokenIndex < n, "swapMint: idx"); - require(maxAmountIn > 0, "swapMint: input zero"); - require(deadline == 0 || block.timestamp <= deadline, "swapMint: deadline"); + ) external returns (uint256 lpMinted) { + bytes memory data = abi.encodeWithSignature( + "swapMint(address,address,uint256,uint256,uint256,uint256)", + payer, + receiver, + inputTokenIndex, + maxAmountIn, + deadline, + swapFeePpm + ); - // Ensure pool initialized - require(lmsr.nAssets > 0, "swapMint: uninit pool"); - - // compute fee on gross maxAmountIn to get an initial net estimate (we'll recompute based on actual used) - (, uint256 netUintGuess) = _computeFee(maxAmountIn); - - // Convert the net guess to internal (floor) - int128 netInternalGuess = _uintToInternalFloor(netUintGuess, bases[inputTokenIndex]); - require(netInternalGuess > int128(0), "swapMint: input too small after fee"); - - // Use LMSR view to determine actual internal consumed and size-increase (ΔS) for mint - (int128 amountInInternalUsed, int128 sizeIncreaseInternal) = lmsr.swapAmountsForMint(inputTokenIndex, netInternalGuess); - - // amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer - uint256 amountInUint = _internalToUintCeil(amountInInternalUsed, bases[inputTokenIndex]); - require(amountInUint > 0, "swapMint: input zero after internal conversion"); - - // Compute fee on the actual used input and total transfer amount (ceiling) - uint256 feeUintActual = _ceilFee(amountInUint, swapFeePpm); - uint256 totalTransfer = amountInUint + feeUintActual; - require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMint: transfer exceeds max"); - - // Record pre-balance and transfer tokens from payer, require exact receipt (revert on fee-on-transfer) - uint256 prevBalI = IERC20(tokens[inputTokenIndex]).balanceOf(address(this)); - tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransfer); - uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this)); - require(balIAfter == prevBalI + totalTransfer, "swapMint: non-standard tokenIn"); - - // Update cached uint balances for token inputTokenIndex (only inputTokenIndex changed externally) - cachedUintBalances[inputTokenIndex] = balIAfter; - - // Compute old and new scaled size metrics to determine LP minted - int128 oldTotal = _computeSizeMetric(lmsr.qInternal); - require(oldTotal > int128(0), "swapMint: zero total"); - uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE); - - int128 newTotal = oldTotal.add(sizeIncreaseInternal); - uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE); - - uint256 actualLpToMint; - if (totalSupply() == 0) { - // If somehow supply zero (shouldn't happen as lmsr.nAssets>0), mint newScaled - actualLpToMint = newScaled; - } else { - require(oldScaled > 0, "swapMint: oldScaled zero"); - uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0; - if (delta > 0) { - // floor truncation rounds in favor of pool - actualLpToMint = (totalSupply() * delta) / oldScaled; - } else { - actualLpToMint = 0; - } - } - - require(actualLpToMint > 0, "swapMint: zero LP minted"); - - // Update LMSR internal state: scale qInternal proportionally by newTotal/oldTotal - int128[] memory newQInternal = new int128[](n); - for (uint256 idx = 0; idx < n; idx++) { - // newQInternal[idx] = qInternal[idx] * (newTotal / oldTotal) - newQInternal[idx] = lmsr.qInternal[idx].mul(newTotal).div(oldTotal); - } - - // Update cached internal and kappa via updateForProportionalChange - lmsr.updateForProportionalChange(newQInternal); - - // Note: we updated cachedUintBalances[inputTokenIndex] above via reading balance; other token uint balances did not - // change externally (they were not transferred in). We keep cachedUintBalances for others unchanged. - // Mint LP tokens to receiver - _mint(receiver, actualLpToMint); - - // Emit SwapMint event with gross transfer, net input and fee (planned exact-in) - emit SwapMint(payer, receiver, inputTokenIndex, totalTransfer, amountInUint, feeUintActual); - - // Emit standard Mint event which records deposit amounts and LP minted - emit Mint(payer, receiver, new uint256[](n), actualLpToMint); - // Note: depositAmounts array omitted (empty) since swapMint uses single-token input - - return actualLpToMint; + bytes memory result = Address.functionDelegateCall(swapMintImpl, data); + return abi.decode(result, (uint256)); } /// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver. - /// @dev The function burns LP tokens (authorization via allowance if needed), sends the single-asset payout and updates LMSR state. + /// @dev This function forwards the call to the burnSwap implementation via delegatecall /// @param payer who burns LP tokens /// @param receiver who receives the single asset /// @param lpAmount amount of LP tokens to burn @@ -753,61 +650,19 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { uint256 lpAmount, uint256 inputTokenIndex, uint256 deadline - ) external nonReentrant returns (uint256 amountOutUint) { - uint256 n = tokens.length; - require(inputTokenIndex < n, "burnSwap: idx"); - require(lpAmount > 0, "burnSwap: zero lp"); - require(deadline == 0 || block.timestamp <= deadline, "burnSwap: deadline"); + ) external returns (uint256 amountOutUint) { + bytes memory data = abi.encodeWithSignature( + "burnSwap(address,address,uint256,uint256,uint256,uint256)", + payer, + receiver, + lpAmount, + inputTokenIndex, + deadline, + swapFeePpm + ); - uint256 supply = totalSupply(); - require(supply > 0, "burnSwap: empty supply"); - require(balanceOf(payer) >= lpAmount, "burnSwap: insufficient LP"); - - // alpha = lpAmount / supply as Q64.64 - int128 alpha = ABDKMath64x64.divu(lpAmount, supply); - - // Use LMSR view to compute single-asset payout and burned size-metric - (int128 payoutInternal, ) = lmsr.swapAmountsForBurn(inputTokenIndex, alpha); - - // Convert payoutInternal -> uint (floor) to favor pool - amountOutUint = _internalToUintFloor(payoutInternal, bases[inputTokenIndex]); - require(amountOutUint > 0, "burnSwap: output zero"); - - // Transfer the payout to receiver - tokens[inputTokenIndex].safeTransfer(receiver, amountOutUint); - - // Burn LP tokens from payer (authorization via allowance) - if (msg.sender != payer) { - uint256 allowed = allowance(payer, msg.sender); - require(allowed >= lpAmount, "burnSwap: allowance insufficient"); - _approve(payer, msg.sender, allowed - lpAmount); - } - _burn(payer, lpAmount); - - // Update cached balances by reading on-chain balances for all tokens - int128[] memory newQInternal = new int128[](n); - for (uint256 idx = 0; idx < n; idx++) { - uint256 bal = IERC20(tokens[idx]).balanceOf(address(this)); - cachedUintBalances[idx] = bal; - newQInternal[idx] = _uintToInternalFloor(bal, bases[idx]); - } - - // Emit BurnSwap with public-facing info only (do not expose ΔS or LP burned) - emit BurnSwap(payer, receiver, inputTokenIndex, amountOutUint); - - // If entire pool drained, deinit; else update proportionally - bool allZero = true; - for (uint256 idx = 0; idx < n; idx++) { - if (newQInternal[idx] != int128(0)) { allZero = false; break; } - } - if (allZero) { - lmsr.deinit(); - } else { - lmsr.updateForProportionalChange(newQInternal); - } - - emit Burn(payer, receiver, new uint256[](n), lpAmount); - return amountOutUint; + bytes memory result = Address.functionDelegateCall(swapMintImpl, data); + return abi.decode(result, (uint256)); } @@ -831,6 +686,8 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { /// @param recipient The address which will receive the token amounts /// @param amounts The amount of each token to send (array length must equal pool size) /// @param data Any data to be passed through to the callback + // todo gas-efficient single-asset flash + // todo fix this func's gas function flash( address recipient, uint256[] memory amounts, @@ -890,37 +747,7 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { } - /* ---------------------- - Conversion helpers - ---------------------- */ - - // Convert uint token amount -> internal 64.64 (floor). Uses ABDKMath64x64.divu which truncates. - function _uintToInternalFloor(uint256 amount, uint256 base) internal pure returns (int128) { - // internal = amount / base (as Q64.64) - return ABDKMath64x64.divu(amount, base); - } - - // Convert internal 64.64 -> uint token amount (floor). Uses ABDKMath64x64.mulu which floors the product. - function _internalToUintFloor(int128 internalAmount, uint256 base) internal pure returns (uint256) { - // uint = internal * base (floored) - return ABDKMath64x64.mulu(internalAmount, base); - } - - // Convert internal 64.64 -> uint token amount (ceiling). Rounds up to protect the pool. - function _internalToUintCeil(int128 internalAmount, uint256 base) internal pure returns (uint256) { - // Get the floor value first - uint256 floorValue = ABDKMath64x64.mulu(internalAmount, base); - - // Check if there was any fractional part by comparing to a reconstruction of the original - int128 reconstructed = ABDKMath64x64.divu(floorValue, base); - - // If reconstructed is less than original, there was a fractional part that was truncated - if (reconstructed < internalAmount) { - return floorValue + 1; - } - - return floorValue; - } + /* Conversion helpers moved to PartyPoolBase (abstract) to centralize internal helpers and storage. */ /// @notice Marginal price of `base` in terms of `quote` (p_quote / p_base) as Q64.64 /// @dev Returns the LMSR marginal price directly (raw 64.64) for use by off-chain quoting logic. @@ -958,15 +785,4 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { return pricePerQ.mul(factor); } - /// @notice Helper to compute size metric (sum of all asset quantities) from internal balances - /// @dev Returns the sum of all provided qInternal_ entries as a Q64.64 value. - function _computeSizeMetric(int128[] memory qInternal_) private pure returns (int128) { - int128 total = int128(0); - for (uint i = 0; i < qInternal_.length; ) { - total = total.add(qInternal_[i]); - unchecked { i++; } - } - return total; - } - } diff --git a/src/PartyPoolBase.sol b/src/PartyPoolBase.sol new file mode 100644 index 0000000..a023888 --- /dev/null +++ b/src/PartyPoolBase.sol @@ -0,0 +1,118 @@ +// 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/ERC20.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "./LMSRStabilized.sol"; + +/// @notice Abstract base contract that contains storage and internal helpers only. +/// No external/public functions or constructor here — derived implementations own immutables and constructors. +abstract contract PartyPoolBase is ERC20, ReentrancyGuard { + using ABDKMath64x64 for int128; + using LMSRStabilized for LMSRStabilized.State; + + // + // Internal state (no immutables here; immutables belong to derived contracts) + // + + // 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 + + /// @notice Per-token uint base denominators used to convert uint token amounts <-> internal Q64.64 representation. + /// @dev denominators()[i] is the base for tokens[i]. These bases are chosen by deployer and must match token decimals. + uint256[] internal bases; // per-token uint base used to scale token amounts <-> internal + + /// @notice Mapping from token address => (index+1). A zero value indicates the token is not in the pool. + /// @dev Use index = tokenAddressToIndexPlusOne[token] - 1 when non-zero. + mapping(IERC20=>uint) internal tokenAddressToIndexPlusOne; // Uses index+1 so a result of 0 indicates a failed lookup + + // Cached on-chain balances (uint) and internal 64.64 representation + // balance / base = internal + uint256[] internal cachedUintBalances; + + + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} + + + /* ---------------------- + Conversion & fee helpers (internal) + ---------------------- */ + + // Convert uint token amount -> internal 64.64 (floor). Uses ABDKMath64x64.divu which truncates. + function _uintToInternalFloor(uint256 amount, uint256 base) internal pure returns (int128) { + // internal = amount / base (as Q64.64) + return ABDKMath64x64.divu(amount, base); + } + + // Convert internal 64.64 -> uint token amount (floor). Uses ABDKMath64x64.mulu which floors the product. + function _internalToUintFloor(int128 internalAmount, uint256 base) internal pure returns (uint256) { + // uint = internal * base (floored) + return ABDKMath64x64.mulu(internalAmount, base); + } + + // Convert internal 64.64 -> uint token amount (ceiling). Rounds up to protect the pool. + function _internalToUintCeil(int128 internalAmount, uint256 base) internal pure returns (uint256) { + // Get the floor value first + uint256 floorValue = ABDKMath64x64.mulu(internalAmount, base); + + // Check if there was any fractional part by comparing to a reconstruction of the original + int128 reconstructed = ABDKMath64x64.divu(floorValue, base); + + // If reconstructed is less than original, there was a fractional part that was truncated + if (reconstructed < internalAmount) { + return floorValue + 1; + } + + return floorValue; + } + + /// @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) { + if (feePpm == 0) return 0; + // ceil division: (num + denom - 1) / denom + return (x * feePpm + 1_000_000 - 1) / 1_000_000; + } + + /// @notice Compute fee and net amounts for a gross input (fee rounded up to favor the pool). + /// @param gross total gross input + /// @param feePpm fee in ppm to apply + /// @return feeUint fee taken (uint) and netUint remaining for protocol use (uint) + function _computeFee(uint256 gross, uint256 feePpm) internal pure returns (uint256 feeUint, uint256 netUint) { + if (feePpm == 0) { + return (0, gross); + } + feeUint = _ceilFee(gross, feePpm); + netUint = gross - feeUint; + } + + /// @notice Convenience: return gross = net + fee(net) using ceiling for fee. + /// @param netUint net amount + /// @param feePpm fee in ppm to apply + function _addFee(uint256 netUint, uint256 feePpm) internal pure returns (uint256 gross) { + if (feePpm == 0) return netUint; + uint256 fee = _ceilFee(netUint, feePpm); + return netUint + fee; + } + + /// @notice Helper to compute size metric (sum of all asset quantities) from internal balances + /// @dev Returns the sum of all provided qInternal_ entries as a Q64.64 value. + function _computeSizeMetric(int128[] memory qInternal_) internal pure returns (int128) { + int128 total = int128(0); + for (uint i = 0; i < qInternal_.length; ) { + total = total.add(qInternal_[i]); + unchecked { i++; } + } + return total; + } +} diff --git a/src/PartyPoolSwapMintImpl.sol b/src/PartyPoolSwapMintImpl.sol new file mode 100644 index 0000000..87decab --- /dev/null +++ b/src/PartyPoolSwapMintImpl.sol @@ -0,0 +1,203 @@ +// 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 PartyPoolSwapMintImpl - Implementation contract for swapMint and burnSwap functions +/// @notice This contract contains the swapMint and burnSwap implementation that will be called via delegatecall +/// @dev This contract inherits from PartyPoolBase to access storage and internal functions +contract PartyPoolSwapMintImpl is PartyPoolBase { + using ABDKMath64x64 for int128; + using LMSRStabilized for LMSRStabilized.State; + using SafeERC20 for IERC20; + + // Events that mirror the main contract events + event SwapMint(address indexed payer, address indexed receiver, uint256 indexed inputTokenIndex, uint256 totalTransfer, uint256 amountInUint, uint256 feeUintActual); + event BurnSwap(address indexed payer, address indexed receiver, uint256 indexed inputTokenIndex, uint256 amountOutUint); + 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 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. + /// @param payer who transfers the input token + /// @param receiver who receives the minted LP tokens + /// @param inputTokenIndex index of the input token + /// @param maxAmountIn maximum uint token input (inclusive of fee) + /// @param deadline optional deadline + /// @param swapFeePpm fee in parts-per-million for this pool + /// @return lpMinted actual LP minted (uint) + function swapMint( + address payer, + address receiver, + uint256 inputTokenIndex, + uint256 maxAmountIn, + uint256 deadline, + uint256 swapFeePpm + ) external returns (uint256 lpMinted) { + uint256 n = tokens.length; + require(inputTokenIndex < n, "swapMint: idx"); + require(maxAmountIn > 0, "swapMint: input zero"); + require(deadline == 0 || block.timestamp <= deadline, "swapMint: deadline"); + + // Ensure pool initialized + require(lmsr.nAssets > 0, "swapMint: uninit pool"); + + // compute fee on gross maxAmountIn to get an initial net estimate (we'll recompute based on actual used) + (, uint256 netUintGuess) = _computeFee(maxAmountIn, swapFeePpm); + + // Convert the net guess to internal (floor) + int128 netInternalGuess = _uintToInternalFloor(netUintGuess, bases[inputTokenIndex]); + require(netInternalGuess > int128(0), "swapMint: input too small after fee"); + + // Use LMSR view to determine actual internal consumed and size-increase (ΔS) for mint + (int128 amountInInternalUsed, int128 sizeIncreaseInternal) = lmsr.swapAmountsForMint(inputTokenIndex, netInternalGuess); + + // amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer + uint256 amountInUint = _internalToUintCeil(amountInInternalUsed, bases[inputTokenIndex]); + require(amountInUint > 0, "swapMint: input zero after internal conversion"); + + // Compute fee on the actual used input and total transfer amount (ceiling) + uint256 feeUintActual = _ceilFee(amountInUint, swapFeePpm); + uint256 totalTransfer = amountInUint + feeUintActual; + require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMint: transfer exceeds max"); + + // Record pre-balance and transfer tokens from payer, require exact receipt (revert on fee-on-transfer) + uint256 prevBalI = IERC20(tokens[inputTokenIndex]).balanceOf(address(this)); + tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransfer); + uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this)); + require(balIAfter == prevBalI + totalTransfer, "swapMint: non-standard tokenIn"); + + // Update cached uint balances for token inputTokenIndex (only inputTokenIndex changed externally) + cachedUintBalances[inputTokenIndex] = balIAfter; + + // Compute old and new scaled size metrics to determine LP minted + int128 oldTotal = _computeSizeMetric(lmsr.qInternal); + require(oldTotal > int128(0), "swapMint: zero total"); + uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE); + + int128 newTotal = oldTotal.add(sizeIncreaseInternal); + uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE); + + uint256 actualLpToMint; + // Use natural ERC20 function since base contract inherits from ERC20 + uint256 currentSupply = totalSupply(); + if (currentSupply == 0) { + // If somehow supply zero (shouldn't happen as lmsr.nAssets>0), mint newScaled + actualLpToMint = newScaled; + } else { + require(oldScaled > 0, "swapMint: oldScaled zero"); + uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0; + if (delta > 0) { + // floor truncation rounds in favor of pool + actualLpToMint = (currentSupply * delta) / oldScaled; + } else { + actualLpToMint = 0; + } + } + + require(actualLpToMint > 0, "swapMint: zero LP minted"); + + // Update LMSR internal state: scale qInternal proportionally by newTotal/oldTotal + int128[] memory newQInternal = new int128[](n); + for (uint256 idx = 0; idx < n; idx++) { + // newQInternal[idx] = qInternal[idx] * (newTotal / oldTotal) + newQInternal[idx] = lmsr.qInternal[idx].mul(newTotal).div(oldTotal); + } + + // Update cached internal and kappa via updateForProportionalChange + lmsr.updateForProportionalChange(newQInternal); + + // Use natural ERC20 function since base contract inherits from ERC20 + _mint(receiver, actualLpToMint); + + // Emit SwapMint event with gross transfer, net input and fee (planned exact-in) + emit SwapMint(payer, receiver, inputTokenIndex, totalTransfer, amountInUint, feeUintActual); + + // Emit standard Mint event which records deposit amounts and LP minted + emit Mint(payer, receiver, new uint256[](n), actualLpToMint); + // Note: depositAmounts array omitted (empty) since swapMint uses single-token input + + return actualLpToMint; + } + + /// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver. + /// @dev The function burns LP tokens (authorization via allowance if needed), sends the single-asset payout and updates LMSR state. + /// @param payer who burns LP tokens + /// @param receiver who receives the single asset + /// @param lpAmount amount of LP tokens to burn + /// @param inputTokenIndex index of target asset to receive + /// @param deadline optional deadline + /// @param swapFeePpm fee in parts-per-million for this pool (may be used for future fee logic) + /// @return amountOutUint uint amount of asset i sent to receiver + // todo fee!? + function burnSwap( + address payer, + address receiver, + uint256 lpAmount, + uint256 inputTokenIndex, + uint256 deadline, + uint256 swapFeePpm + ) external returns (uint256 amountOutUint) { + uint256 n = tokens.length; + require(inputTokenIndex < n, "burnSwap: idx"); + require(lpAmount > 0, "burnSwap: zero lp"); + require(deadline == 0 || block.timestamp <= deadline, "burnSwap: deadline"); + + uint256 supply = totalSupply(); + require(supply > 0, "burnSwap: empty supply"); + require(balanceOf(payer) >= lpAmount, "burnSwap: insufficient LP"); + + // alpha = lpAmount / supply as Q64.64 + int128 alpha = ABDKMath64x64.divu(lpAmount, supply); + + // Use LMSR view to compute single-asset payout and burned size-metric + (int128 payoutInternal, ) = lmsr.swapAmountsForBurn(inputTokenIndex, alpha); + + // Convert payoutInternal -> uint (floor) to favor pool + amountOutUint = _internalToUintFloor(payoutInternal, bases[inputTokenIndex]); + require(amountOutUint > 0, "burnSwap: output zero"); + + // Transfer the payout to receiver + tokens[inputTokenIndex].safeTransfer(receiver, amountOutUint); + + // Burn LP tokens from payer (authorization via allowance) + if (msg.sender != payer) { + uint256 allowed = allowance(payer, msg.sender); + require(allowed >= lpAmount, "burnSwap: allowance insufficient"); + _approve(payer, msg.sender, allowed - lpAmount); + } + _burn(payer, lpAmount); + + // Update cached balances by reading on-chain balances for all tokens + int128[] memory newQInternal = new int128[](n); + for (uint256 idx = 0; idx < n; idx++) { + uint256 bal = IERC20(tokens[idx]).balanceOf(address(this)); + cachedUintBalances[idx] = bal; + newQInternal[idx] = _uintToInternalFloor(bal, bases[idx]); + } + + // Emit BurnSwap with public-facing info only (do not expose ΔS or LP burned) + emit BurnSwap(payer, receiver, inputTokenIndex, amountOutUint); + + // If entire pool drained, deinit; else update proportionally + bool allZero = true; + for (uint256 idx = 0; idx < n; idx++) { + if (newQInternal[idx] != int128(0)) { allZero = false; break; } + } + if (allZero) { + lmsr.deinit(); + } else { + lmsr.updateForProportionalChange(newQInternal); + } + + emit Burn(payer, receiver, new uint256[](n), lpAmount); + return amountOutUint; + } +} diff --git a/test/GasTest.sol b/test/GasTest.sol index 8cd4ea4..ec2db47 100644 --- a/test/GasTest.sol +++ b/test/GasTest.sol @@ -6,9 +6,9 @@ import "@abdk/ABDKMath64x64.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "../src/LMSRStabilized.sol"; import "../src/PartyPool.sol"; - -// Import the flash callback interface +import "../src/PartyPlanner.sol"; import "../src/IPartyFlashCallback.sol"; +import {Deploy} from "../src/Deploy.sol"; /// @notice Test contract that implements the flash callback for testing flash loans contract FlashBorrower is IPartyFlashCallback { @@ -130,6 +130,7 @@ contract TestERC20 is ERC20 { contract GasTest is Test { using ABDKMath64x64 for int128; + PartyPlanner planner; PartyPool pool2; PartyPool pool10; PartyPool pool20; @@ -172,7 +173,7 @@ contract GasTest is Test { } // Compute kappa from slippage params and number of tokens, then construct pool with kappa int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(ierc20Tokens.length, tradeFrac, targetSlippage); - PartyPool newPool = new PartyPool(poolName, poolName, ierc20Tokens, bases, computedKappa, feePpm, feePpm, false); + PartyPool newPool = Deploy.newPartyPool(poolName, poolName, ierc20Tokens, bases, computedKappa, feePpm, feePpm, false); // Transfer initial deposit amounts into pool before initial mint for (uint256 i = 0; i < numTokens; i++) { @@ -212,7 +213,7 @@ contract GasTest is Test { ierc20Tokens[i] = IERC20(tokens[i]); } int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(ierc20Tokens.length, tradeFrac, targetSlippage); - PartyPool newPool = new PartyPool(poolName, poolName, ierc20Tokens, bases, computedKappa, feePpm, feePpm, true); + PartyPool newPool = Deploy.newPartyPool(poolName, poolName, ierc20Tokens, bases, computedKappa, feePpm, feePpm, true); // Transfer initial deposit amounts into pool before initial mint for (uint256 i = 0; i < numTokens; i++) { @@ -229,6 +230,8 @@ contract GasTest is Test { alice = address(0xA11ce); bob = address(0xB0b); + planner = Deploy.newPartyPlanner(); + // Configure LMSR parameters similar to other tests: trade size 1% of asset -> 0.01, slippage 0.001 tradeFrac = ABDKMath64x64.divu(100, 10_000); // 0.01 targetSlippage = ABDKMath64x64.divu(10, 10_000); // 0.001 diff --git a/test/PartyPlanner.t.sol b/test/PartyPlanner.t.sol index 1020eac..a1ed953 100644 --- a/test/PartyPlanner.t.sol +++ b/test/PartyPlanner.t.sol @@ -5,6 +5,7 @@ import "forge-std/Test.sol"; import "../src/LMSRStabilized.sol"; import "../src/PartyPlanner.sol"; import "../src/PartyPool.sol"; +import {Deploy} from "../src/Deploy.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; // Mock ERC20 token for testing @@ -38,7 +39,7 @@ contract PartyPlannerTest is Test { function setUp() public { // Deploy PartyPlanner - planner = new PartyPlanner(); + planner = Deploy.newPartyPlanner(); // Deploy mock tokens tokenA = new MockERC20("Token A", "TKNA", 18); @@ -87,7 +88,7 @@ contract PartyPlannerTest is Test { // Compute kappa then create pool via kappa overload int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); - (PartyPool pool, uint256 lpAmount) = planner.createPool( + (PartyPool pool, uint256 lpAmount) = planner.newPool( name, symbol, tokens, @@ -166,7 +167,7 @@ contract PartyPlannerTest is Test { deposits1[1] = INITIAL_DEPOSIT_AMOUNT; int128 kappa1 = LMSRStabilized.computeKappaFromSlippage(tokens1.length, int128((1 << 64) - 1), int128(1 << 62)); - (PartyPool pool1,) = planner.createPool( + (PartyPool pool1,) = planner.newPool( "Pool 1", "LP1", tokens1, bases1, kappa1, 3000, 5000, false, payer, receiver, deposits1, 1000e18, 0 @@ -186,7 +187,7 @@ contract PartyPlannerTest is Test { deposits2[1] = INITIAL_DEPOSIT_AMOUNT / 1e12; // Adjust for 6 decimals int128 kappa2 = LMSRStabilized.computeKappaFromSlippage(tokens2.length, int128((1 << 64) - 1), int128(1 << 62)); - (PartyPool pool2,) = planner.createPool( + (PartyPool pool2,) = planner.newPool( "Pool 2", "LP2", tokens2, bases2, kappa2, 3000, 5000, false, payer, receiver, deposits2, 1000e18, 0 @@ -230,7 +231,7 @@ contract PartyPlannerTest is Test { // Test token/deposit length mismatch vm.expectRevert("Planner: tokens and deposits length mismatch"); // call old-signature convenience (it will still exist) for the mismatched-length revert check - planner.createPool( + planner.newPool( "Test Pool", "TESTLP", tokens, bases, int128((1 << 64) - 1), int128(1 << 62), 3000, 5000, false, payer, receiver, deposits, 1000e18, 0 @@ -244,7 +245,7 @@ contract PartyPlannerTest is Test { int128 kappaErr = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62)); vm.expectRevert("Planner: payer cannot be zero address"); - planner.createPool( + planner.newPool( "Test Pool", "TESTLP", tokens, bases, kappaErr, 3000, 5000, false, address(0), receiver, validDeposits, 1000e18, 0 @@ -252,7 +253,7 @@ contract PartyPlannerTest is Test { // Test zero receiver address vm.expectRevert("Planner: receiver cannot be zero address"); - planner.createPool( + planner.newPool( "Test Pool", "TESTLP", tokens, bases, kappaErr, 3000, 5000, false, payer, address(0), validDeposits, 1000e18, 0 @@ -263,7 +264,7 @@ contract PartyPlannerTest is Test { int128 kappaDeadline = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62)); vm.warp(1000); vm.expectRevert("Planner: deadline exceeded"); - planner.createPool( + planner.newPool( "Test Pool", "TESTLP", tokens, bases, kappaDeadline, 3000, 5000, false, payer, receiver, validDeposits, 1000e18, block.timestamp - 1 @@ -289,7 +290,7 @@ contract PartyPlannerTest is Test { deposits[1] = INITIAL_DEPOSIT_AMOUNT; int128 kappaLoop = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62)); - (PartyPool pool,) = planner.createPool( + (PartyPool pool,) = planner.newPool( string(abi.encodePacked("Pool ", vm.toString(i))), string(abi.encodePacked("LP", vm.toString(i))), tokens, bases, diff --git a/test/PartyPool.t.sol b/test/PartyPool.t.sol index 3f60345..62fcc2f 100644 --- a/test/PartyPool.t.sol +++ b/test/PartyPool.t.sol @@ -9,6 +9,8 @@ import "../src/PartyPool.sol"; // Import the flash callback interface import "../src/IPartyFlashCallback.sol"; +import {PartyPlanner} from "../src/PartyPlanner.sol"; +import {Deploy} from "../src/Deploy.sol"; /// @notice Test contract that implements the flash callback for testing flash loans contract FlashBorrower is IPartyFlashCallback { @@ -137,6 +139,7 @@ contract PartyPoolTest is Test { TestERC20 token7; TestERC20 token8; TestERC20 token9; + PartyPlanner planner; PartyPool pool; PartyPool pool10; @@ -151,6 +154,7 @@ contract PartyPoolTest is Test { uint256 constant BASE = 1; // use base=1 so internal amounts correspond to raw integers (Q64.64 units) function setUp() public { + planner = Deploy.newPartyPlanner(); alice = address(0xA11ce); bob = address(0xB0b); @@ -197,7 +201,7 @@ contract PartyPoolTest is Test { uint256 feePpm = 1000; int128 kappa3 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); - pool = new PartyPool("LP", "LP", tokens, bases, kappa3, feePpm, feePpm, false); + pool = Deploy.newPartyPool("LP", "LP", tokens, bases, kappa3, feePpm, feePpm, false); // Transfer initial deposit amounts into pool before initial mint (pool expects tokens already in contract) // We deposit equal amounts INIT_BAL for each token @@ -227,7 +231,7 @@ contract PartyPoolTest is Test { } int128 kappa10 = LMSRStabilized.computeKappaFromSlippage(tokens10.length, tradeFrac, targetSlippage); - pool10 = new PartyPool("LP10", "LP10", tokens10, bases10, kappa10, feePpm, feePpm, false); + pool10 = Deploy.newPartyPool("LP10", "LP10", tokens10, bases10, kappa10, feePpm, feePpm, false); // Mint additional tokens for pool10 initial deposit token0.mint(address(this), INIT_BAL); @@ -1231,11 +1235,11 @@ contract PartyPoolTest is Test { // Pool with default initialization (lpTokens = 0) int128 kappaDefault = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); - PartyPool poolDefault = new PartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault, feePpm, feePpm, false); + PartyPool poolDefault = Deploy.newPartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault, feePpm, feePpm, false); // Pool with custom initialization (lpTokens = custom amount) int128 kappaCustom = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); - PartyPool poolCustom = new PartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom, feePpm, feePpm, false); + PartyPool poolCustom = Deploy.newPartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom, feePpm, feePpm, false); // Mint additional tokens for both pools token0.mint(address(this), INIT_BAL * 2); @@ -1307,9 +1311,9 @@ contract PartyPoolTest is Test { uint256 feePpm = 1000; int128 kappaDefault2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); - PartyPool poolDefault = new PartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault2, feePpm, feePpm, false); + PartyPool poolDefault = Deploy.newPartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault2, feePpm, feePpm, false); int128 kappaCustom2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); - PartyPool poolCustom = new PartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom2, feePpm, feePpm, false); + PartyPool poolCustom = Deploy.newPartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom2, feePpm, feePpm, false); // Mint additional tokens token0.mint(address(this), INIT_BAL * 4); @@ -1329,7 +1333,7 @@ contract PartyPoolTest is Test { uint256 lpDefault = poolDefault.initialMint(address(this), 0); uint256 scaleFactor = 3; uint256 customLpAmount = lpDefault * scaleFactor; - uint256 lpCustom = poolCustom.initialMint(address(this), customLpAmount); + poolCustom.initialMint(address(this), customLpAmount); // Verify initial LP supplies assertEq(poolDefault.totalSupply(), lpDefault, "Default pool should have default LP supply");