// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.30; import "./IPartyPlanner.sol"; 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"; import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol"; /// @title PartyPlanner /// @notice Factory contract for creating and tracking PartyPool instances contract PartyPlanner is IPartyPlanner { using SafeERC20 for IERC20; int128 private constant ONE = int128(1) << 64; /// @notice Address of the Mint implementation contract used by all pools created by this factory PartyPoolMintImpl private immutable MINT_IMPL; function mintImpl() external view returns (PartyPoolMintImpl) { return MINT_IMPL; } /// @notice Address of the SwapMint implementation contract used by all pools created by this factory PartyPoolSwapMintImpl private immutable SWAP_MINT_IMPL; function swapMintImpl() external view returns (PartyPoolSwapMintImpl) { return SWAP_MINT_IMPL; } // On-chain pool indexing PartyPool[] private _allPools; IERC20[] private _allTokens; mapping(PartyPool => bool) private _poolSupported; mapping(IERC20 => bool) private _tokenSupported; mapping(IERC20 => PartyPool[]) private _poolsByToken; /// @param _swapMintImpl address of the SwapMint implementation contract to be used by all pools /// @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"); SWAP_MINT_IMPL = _swapMintImpl; require(address(_mintImpl) != address(0), "Planner: mintImpl address cannot be zero"); MINT_IMPL = _mintImpl; } /// Main newPool variant: accepts kappa directly (preferred). function newPool( // Pool constructor args string memory name_, string memory symbol_, IERC20[] memory _tokens, uint256[] memory _bases, int128 _kappa, uint256 _swapFeePpm, uint256 _flashFeePpm, bool _stable, // Initial deposit information address payer, address receiver, uint256[] memory initialDeposits, uint256 initialLpAmount, uint256 deadline ) public returns (PartyPool pool, uint256 lpAmount) { // Validate inputs require(deadline == 0 || block.timestamp <= deadline, "Planner: deadline exceeded"); require(_tokens.length == initialDeposits.length, "Planner: tokens and deposits length mismatch"); require(payer != address(0), "Planner: payer cannot be zero address"); require(receiver != address(0), "Planner: receiver cannot be zero address"); // Validate kappa > 0 (Q64.64) require(_kappa > int128(0), "Planner: kappa must be > 0"); // Create a new PartyPool instance (kappa-based constructor) pool = new PartyPool( name_, symbol_, _tokens, _bases, _kappa, _swapFeePpm, _flashFeePpm, _stable, PartyPoolSwapMintImpl(SWAP_MINT_IMPL), MINT_IMPL ); _allPools.push(pool); _poolSupported[pool] = true; // Track tokens and populate mappings for (uint256 i = 0; i < _tokens.length; i++) { IERC20 token = _tokens[i]; // Add token to _allTokens if not already present if (!_tokenSupported[token]) { _allTokens.push(token); _tokenSupported[token] = true; } // Add pool to _poolsByToken mapping _poolsByToken[token].push(pool); } emit PartyStarted(pool, name_, symbol_, _tokens); // Transfer initial tokens from payer to the pool for (uint256 i = 0; i < _tokens.length; i++) { if (initialDeposits[i] > 0) { IERC20(_tokens[i]).safeTransferFrom(payer, address(pool), initialDeposits[i]); } } // Call mint on the new pool to initialize it with the transferred tokens lpAmount = pool.initialMint(receiver, initialLpAmount); } // 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_, IERC20[] memory _tokens, uint256[] memory _bases, int128 _tradeFrac, int128 _targetSlippage, uint256 _swapFeePpm, uint256 _flashFeePpm, bool _stable, // Initial deposit information address payer, address receiver, uint256[] memory initialDeposits, uint256 initialLpAmount, uint256 deadline ) external returns (PartyPool pool, uint256 lpAmount) { // Validate fixed-point fractions: must be less than 1.0 in 64.64 fixed-point require(_tradeFrac < ONE, "Planner: tradeFrac must be < 1 (64.64)"); require(_targetSlippage < ONE, "Planner: targetSlippage must be < 1 (64.64)"); // 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 newPool variant return newPool( name_, symbol_, _tokens, _bases, computedKappa, _swapFeePpm, _flashFeePpm, _stable, payer, receiver, initialDeposits, initialLpAmount, deadline ); } /// @inheritdoc IPartyPlanner function getPoolSupported(address pool) external view returns (bool) { return _poolSupported[PartyPool(pool)]; } /// @inheritdoc IPartyPlanner function poolCount() external view returns (uint256) { return _allPools.length; } /// @inheritdoc IPartyPlanner function getAllPools(uint256 offset, uint256 limit) external view returns (PartyPool[] memory pools) { uint256 totalPools = _allPools.length; // If offset is beyond array bounds, return empty array if (offset >= totalPools) { return new PartyPool[](0); } // Calculate actual number of pools to return (respecting bounds) uint256 itemsToReturn = (offset + limit > totalPools) ? (totalPools - offset) : limit; // Create result array of appropriate size pools = new PartyPool[](itemsToReturn); // Fill the result array for (uint256 i = 0; i < itemsToReturn; i++) { pools[i] = _allPools[offset + i]; } return pools; } /// @inheritdoc IPartyPlanner function tokenCount() external view returns (uint256) { return _allTokens.length; } /// @inheritdoc IPartyPlanner function getAllTokens(uint256 offset, uint256 limit) external view returns (address[] memory tokens) { uint256 totalTokens = _allTokens.length; // If offset is beyond array bounds, return empty array if (offset >= totalTokens) { return new address[](0); } // Calculate actual number of tokens to return (respecting bounds) uint256 itemsToReturn = (offset + limit > totalTokens) ? (totalTokens - offset) : limit; // Create result array of appropriate size tokens = new address[](itemsToReturn); // Fill the result array for (uint256 i = 0; i < itemsToReturn; i++) { tokens[i] = address(_allTokens[offset + i]); } return tokens; } /// @inheritdoc IPartyPlanner function poolsByTokenCount(IERC20 token) external view returns (uint256) { return _poolsByToken[token].length; } /// @inheritdoc IPartyPlanner function getPoolsByToken(IERC20 token, uint256 offset, uint256 limit) external view returns (PartyPool[] memory pools) { PartyPool[] storage tokenPools = _poolsByToken[token]; uint256 totalPools = tokenPools.length; // If offset is beyond array bounds, return empty array if (offset >= totalPools) { return new PartyPool[](0); } // Calculate actual number of pools to return (respecting bounds) uint256 itemsToReturn = (offset + limit > totalPools) ? (totalPools - offset) : limit; // Create result array of appropriate size pools = new PartyPool[](itemsToReturn); // Fill the result array for (uint256 i = 0; i < itemsToReturn; i++) { pools[i] = tokenPools[offset + i]; } return pools; } }