270 lines
10 KiB
Solidity
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;
|
|
}
|
|
}
|