Files
lmsr-amm/src/PartyPlanner.sol
2025-10-02 16:43:02 -04:00

270 lines
10 KiB
Solidity

// 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; }
/// @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 private immutable PROTOCOL_FEE_ADDRESS;
function protocolFeeAddress() external view returns (address) { return PROTOCOL_FEE_ADDRESS; }
// 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
/// @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(
PartyPoolSwapMintImpl _swapMintImpl,
PartyPoolMintImpl _mintImpl,
uint256 _protocolFeePpm,
address _protocolFeeAddress
) {
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;
require(_protocolFeePpm < 1_000_000, "Planner: protocol fee >= ppm");
PROTOCOL_FEE_PPM = _protocolFeePpm;
PROTOCOL_FEE_ADDRESS = _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 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,
PROTOCOL_FEE_PPM,
PROTOCOL_FEE_ADDRESS,
_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;
}
}