296 lines
12 KiB
Solidity
296 lines
12 KiB
Solidity
// 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 private immutable PROTOCOL_FEE_ADDRESS;
|
|
function protocolFeeAddress() external view returns (address) { return PROTOCOL_FEE_ADDRESS; }
|
|
|
|
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_;
|
|
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 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,
|
|
PROTOCOL_FEE_ADDRESS,
|
|
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;
|
|
}
|
|
}
|