// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.30; import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import {IPartyPlanner} from "./IPartyPlanner.sol"; import {IPartyPool} from "./IPartyPool.sol"; import {LMSRStabilized} from "./LMSRStabilized.sol"; import {NativeWrapper} from "./NativeWrapper.sol"; import {OwnableExternal} from "./OwnableExternal.sol"; import {OwnableInternal} from "./OwnableInternal.sol"; import {IPartyPoolDeployer} from "./PartyPoolDeployer.sol"; import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol"; import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol"; /// @title PartyPlanner /// @notice Factory contract for creating and tracking PartyPool instances contract PartyPlanner is OwnableExternal, 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 PartyPoolSwapImpl private immutable SWAP_IMPL; function swapImpl() external view returns (PartyPoolSwapImpl) { return SWAP_IMPL; } /// @notice Protocol fee share (ppm) applied to fees collected by pools created by this planner uint256 private immutable PROTOCOL_FEE_PPM; function protocolFeePpm() external view returns (uint256) { return PROTOCOL_FEE_PPM; } /// @notice Address to receive protocol fees for pools created by this planner (may be address(0)) address public protocolFeeAddress; function setProtocolFeeAddress( address feeAddress ) external onlyOwner { protocolFeeAddress = feeAddress; } NativeWrapper private immutable WRAPPER; function wrapper() external view returns (NativeWrapper) { return WRAPPER; } IPartyPoolDeployer private immutable NORMAL_POOL_DEPLOYER; IPartyPoolDeployer private immutable BALANCED_PAIR_DEPLOYER; // On-chain pool indexing IPartyPool[] private _allPools; IERC20[] private _allTokens; mapping(IPartyPool => bool) private _poolSupported; mapping(IERC20 => bool) private _tokenSupported; mapping(IERC20 => IPartyPool[]) private _poolsByToken; /// @param owner_ Initial administrator who is allowed to create new pools and kill() old ones /// @param wrapper_ The WETH9 implementation address used for this chain /// @param swapImpl_ address of the Swap implementation contract to be used by all pools /// @param mintImpl_ address of the Mint implementation contract to be used by all pools /// @param protocolFeePpm_ protocol fee share (ppm) to be used for pools created by this planner /// @param protocolFeeAddress_ recipient address for protocol fees for pools created by this planner (may be address(0)) constructor( address owner_, NativeWrapper wrapper_, PartyPoolSwapImpl swapImpl_, PartyPoolMintImpl mintImpl_, IPartyPoolDeployer deployer_, IPartyPoolDeployer balancedPairDeployer_, uint256 protocolFeePpm_, address protocolFeeAddress_ ) OwnableExternal(owner_) { WRAPPER = wrapper_; require(address(swapImpl_) != address(0), "Planner: swapImpl address cannot be zero"); SWAP_IMPL = swapImpl_; require(address(mintImpl_) != address(0), "Planner: mintImpl address cannot be zero"); MINT_IMPL = mintImpl_; require(address(deployer_) != address(0), "Planner: deployer address cannot be zero"); NORMAL_POOL_DEPLOYER = deployer_; require(address(balancedPairDeployer_) != address(0), "Planner: balanced pair deployer address cannot be zero"); BALANCED_PAIR_DEPLOYER = balancedPairDeployer_; require(protocolFeePpm_ < 1_000_000, "Planner: protocol fee >= ppm"); PROTOCOL_FEE_PPM = protocolFeePpm_; protocolFeeAddress = protocolFeeAddress_; } /// 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 onlyOwner returns (IPartyPool 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) IPartyPoolDeployer deployer = stable_ && tokens_.length == 2 ? BALANCED_PAIR_DEPLOYER : NORMAL_POOL_DEPLOYER; pool = deployer.deploy( _owner, // Same owner as this PartyPlanner name_, symbol_, tokens_, bases_, kappa_, swapFeePpm_, flashFeePpm_, PROTOCOL_FEE_PPM, protocolFeeAddress, WRAPPER, SWAP_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]); require(IERC20(tokens_[i]).balanceOf(address(pool)) == initialDeposits[i], 'fee-on-transfer tokens not supported'); } } // 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 onlyOwner returns (IPartyPool 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[IPartyPool(pool)]; } /// @inheritdoc IPartyPlanner function poolCount() external view returns (uint256) { return _allPools.length; } /// @inheritdoc IPartyPlanner function getAllPools(uint256 offset, uint256 limit) external view returns (IPartyPool[] memory pools) { uint256 totalPools = _allPools.length; // If offset is beyond array bounds, return empty array if (offset >= totalPools) { return new IPartyPool[](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 IPartyPool[](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 (IPartyPool[] memory pools) { IPartyPool[] storage tokenPools = _poolsByToken[token]; uint256 totalPools = tokenPools.length; // If offset is beyond array bounds, return empty array if (offset >= totalPools) { return new IPartyPool[](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 IPartyPool[](itemsToReturn); // Fill the result array for (uint256 i = 0; i < itemsToReturn; i++) { pools[i] = tokenPools[offset + i]; } return pools; } }