// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.30; import "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sol"; import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol"; import {Address} from "../lib/openzeppelin-contracts/contracts/utils/Address.sol"; import {ERC20External} from "./ERC20External.sol"; import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {IPartyFlashCallback} from "./IPartyFlashCallback.sol"; import {IPartyPool} from "./IPartyPool.sol"; import {LMSRStabilizedBalancedPair} from "./LMSRStabilizedBalancedPair.sol"; import {LMSRStabilized} from "./LMSRStabilized.sol"; import {PartyPoolBase} from "./PartyPoolBase.sol"; import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol"; import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol"; import {Proxy} from "../lib/openzeppelin-contracts/contracts/proxy/Proxy.sol"; import {ReentrancyGuard} from "../lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol"; import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import {IERC3156FlashLender} from "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashLender.sol"; /// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token /// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model. /// The pool issues an ERC20 LP token representing proportional ownership. /// It supports: /// - Proportional minting and burning of LP tokens, /// - Single-token mint (swapMint) and single-asset withdrawal (burnSwap), /// - Exact-input swaps and swaps-to-price-limits, /// - Flash loans via a callback interface. /// /// @dev The contract stores per-token uint "bases" used to scale token units into the internal Q64.64 /// representation used by the LMSR library. Cached on-chain uint balances are kept to reduce balanceOf calls. /// The contract uses ceiling/floor rules described in function comments to bias rounding in favor of the pool /// (i.e., floor outputs to users, ceil inputs/fees where appropriate). contract PartyPool is PartyPoolBase, ERC20External, IPartyPool { using ABDKMath64x64 for int128; using LMSRStabilized for LMSRStabilized.State; using SafeERC20 for IERC20; /// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q) /// @dev Pool is constructed with a fixed κ. Clients that previously passed tradeFrac/targetSlippage /// should use LMSRStabilized.computeKappaFromSlippage(...) to derive κ and pass it here. int128 private immutable KAPPA; // kappa in Q64.64 function kappa() external view returns (int128) { return KAPPA; } /// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations. uint256 private immutable SWAP_FEE_PPM; function swapFeePpm() external view returns (uint256) { return SWAP_FEE_PPM; } /// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts. uint256 private immutable FLASH_FEE_PPM; function flashFeePpm() external view returns (uint256) { return FLASH_FEE_PPM; } /// @notice Protocol fee share (ppm) applied to fees collected by the pool (floored when accrued) uint256 private immutable PROTOCOL_FEE_PPM; function protocolFeePpm() external view returns (uint256) { return PROTOCOL_FEE_PPM; } /// @notice Address to which collected protocol tokens will be sent on collectProtocolFees() address private immutable PROTOCOL_FEE_ADDRESS; function protocolFeeAddress() external view returns (address) { return PROTOCOL_FEE_ADDRESS; } // @inheritdoc IPartyPool function allProtocolFeesOwed() external view returns (uint256[] memory) { return protocolFeesOwed; } /// @notice Address of the Mint implementation contract for delegatecall PartyPoolMintImpl private immutable MINT_IMPL; function mintImpl() external view returns (PartyPoolMintImpl) { return MINT_IMPL; } /// @notice Address of the SwapMint implementation contract for delegatecall PartyPoolSwapImpl private immutable SWAP_IMPL; function swapMintImpl() external view returns (PartyPoolSwapImpl) { return SWAP_IMPL; } /// @inheritdoc IPartyPool function getToken(uint256 i) external view returns (IERC20) { return tokens[i]; } /// @inheritdoc IPartyPool function numTokens() external view returns (uint256) { return tokens.length; } /// @inheritdoc IPartyPool function allTokens() external view returns (IERC20[] memory) { return tokens; } /// @inheritdoc IPartyPool function denominators() external view returns (uint256[] memory) { return bases; } function LMSR() external view returns (LMSRStabilized.State memory) { return lmsr; } /// @param name_ LP token name /// @param symbol_ LP token symbol /// @param tokens_ token addresses (n) /// @param bases_ scaling bases for each token (n) - used when converting to/from internal 64.64 amounts /// @param kappa_ liquidity parameter κ (Q64.64) used to derive b = κ * S(q) /// @param swapFeePpm_ fee in parts-per-million, taken from swap input amounts before LMSR calculations /// @param flashFeePpm_ fee in parts-per-million, taken for flash loans /// @param swapImpl_ address of the SwapMint implementation contract /// @param mintImpl_ address of the Mint implementation contract constructor( string memory name_, string memory symbol_, IERC20[] memory tokens_, uint256[] memory bases_, int128 kappa_, uint256 swapFeePpm_, uint256 flashFeePpm_, uint256 protocolFeePpm_, // NEW: protocol share of fees (ppm) address protocolFeeAddress_, // NEW: recipient for collected protocol tokens PartyPoolSwapImpl swapImpl_, PartyPoolMintImpl mintImpl_ ) ERC20External(name_, symbol_) { require(tokens_.length > 1, "Pool: need >1 asset"); require(tokens_.length == bases_.length, "Pool: lengths mismatch"); tokens = tokens_; bases = bases_; KAPPA = kappa_; require(swapFeePpm_ < 1_000_000, "Pool: fee >= ppm"); SWAP_FEE_PPM = swapFeePpm_; require(flashFeePpm_ < 1_000_000, "Pool: flash fee >= ppm"); FLASH_FEE_PPM = flashFeePpm_; require(protocolFeePpm_ < 1_000_000, "Pool: protocol fee >= ppm"); PROTOCOL_FEE_PPM = protocolFeePpm_; PROTOCOL_FEE_ADDRESS = protocolFeeAddress_; SWAP_IMPL = swapImpl_; MINT_IMPL = mintImpl_; uint256 n = tokens_.length; // Initialize LMSR state nAssets; full init occurs on first mint when quantities are known. lmsr.nAssets = n; // Initialize token address to index mapping for (uint i = 0; i < n;) { tokenAddressToIndexPlusOne[tokens_[i]] = i + 1; unchecked {i++;} } // Initialize caches to zero and protocol ledger cachedUintBalances = new uint256[](n); protocolFeesOwed = new uint256[](n); } /* ---------------------- Initialization / Mint / Burn (LP token managed) ---------------------- */ /// @inheritdoc IPartyPool function initialMint(address receiver, uint256 lpTokens) external nonReentrant returns (uint256 lpMinted) { bytes memory data = abi.encodeWithSignature( "initialMint(address,uint256,int128)", receiver, lpTokens, KAPPA ); bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data); return abi.decode(result, (uint256)); } /// @notice Proportional mint for existing pool. /// @dev This function forwards the call to the mint implementation via delegatecall /// @param payer address that provides the input tokens /// @param receiver address that receives the LP tokens /// @param lpTokenAmount desired amount of LP tokens to mint /// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore. function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external nonReentrant returns (uint256 lpMinted) { bytes memory data = abi.encodeWithSignature( "mint(address,address,uint256,uint256)", payer, receiver, lpTokenAmount, deadline ); bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data); return abi.decode(result, (uint256)); } /// @notice Burn LP tokens and withdraw the proportional basket to receiver. /// @dev This function forwards the call to the burn implementation via delegatecall /// @param payer address that provides the LP tokens to burn /// @param receiver address that receives the withdrawn tokens /// @param lpAmount amount of LP tokens to burn (proportional withdrawal) /// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore. function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external nonReentrant returns (uint256[] memory withdrawAmounts) { bytes memory data = abi.encodeWithSignature( "burn(address,address,uint256,uint256)", payer, receiver, lpAmount, deadline ); bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data); return abi.decode(result, (uint256[])); } /* ---------------------- Swaps ---------------------- */ /* function swapAmounts( uint256 inputTokenIndex, uint256 outputTokenIndex, uint256 maxAmountIn, int128 limitPrice ) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) { (uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice); return (grossIn, outUint, feeUint); } */ /// @inheritdoc IPartyPool function swap( address payer, address receiver, uint256 inputTokenIndex, uint256 outputTokenIndex, uint256 maxAmountIn, int128 limitPrice, uint256 deadline ) external nonReentrant returns (uint256 amountIn, uint256 amountOut, uint256 fee) { uint256 n = tokens.length; require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx"); require(maxAmountIn > 0, "swap: input zero"); require(deadline == 0 || block.timestamp <= deadline, "swap: deadline exceeded"); // Read previous balances for affected assets uint256 prevBalI = IERC20(tokens[inputTokenIndex]).balanceOf(address(this)); uint256 prevBalJ = IERC20(tokens[outputTokenIndex]).balanceOf(address(this)); // Compute amounts using the same path as views (uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalUsed, int128 amountOutInternal, , uint256 feeUint) = _quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice); // Transfer the exact amount from payer and require exact receipt (revert on fee-on-transfer) tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount); uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this)); require(balIAfter == prevBalI + totalTransferAmount, "swap: non-standard tokenIn"); // Transfer output to receiver and verify exact decrease tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint); uint256 balJAfter = IERC20(tokens[outputTokenIndex]).balanceOf(address(this)); require(balJAfter == prevBalJ - amountOutUint, "swap: non-standard tokenOut"); // Accrue protocol share (floor) from the fee on input token if (PROTOCOL_FEE_PPM > 0 && feeUint > 0 && PROTOCOL_FEE_ADDRESS != address(0)) { uint256 protoShare = (feeUint * PROTOCOL_FEE_PPM) / 1_000_000; // floor if (protoShare > 0) { protocolFeesOwed[inputTokenIndex] += protoShare; } } // Update cached uint balances for i and j using effective balances (onchain - owed) _recordCachedBalance(inputTokenIndex, balIAfter); _recordCachedBalance(outputTokenIndex, balJAfter); // Apply swap to LMSR state with the internal amounts actually used lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalUsed, amountOutInternal); emit Swap(payer, receiver, tokens[inputTokenIndex], tokens[outputTokenIndex], totalTransferAmount, amountOutUint); return (totalTransferAmount, amountOutUint, feeUint); } /// @notice Internal quote for exact-input swap that mirrors swap() rounding and fee application /// @dev Returns amounts consistent with swap() semantics: grossIn includes fees (ceil), amountOut is floored. /// @return grossIn amount to transfer in (inclusive of fee), amountOutUint output amount (uint), /// amountInInternalUsed and amountOutInternal (64.64), amountInUintNoFee input amount excluding fee (uint), /// feeUint fee taken from the gross input (uint) function _quoteSwapExactIn( uint256 inputTokenIndex, uint256 outputTokenIndex, uint256 maxAmountIn, int128 limitPrice ) internal view returns ( uint256 grossIn, uint256 amountOutUint, int128 amountInInternalUsed, int128 amountOutInternal, uint256 amountInUintNoFee, uint256 feeUint ) { uint256 n = tokens.length; require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx"); require(maxAmountIn > 0, "swap: input zero"); require(lmsr.nAssets > 0, "swap: empty pool"); // Estimate max net input (fee on gross rounded up, then subtract) (, uint256 netUintForSwap) = _computeFee(maxAmountIn, SWAP_FEE_PPM); // Convert to internal (floor) int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, bases[inputTokenIndex]); require(deltaInternalI > int128(0), "swap: input too small after fee"); // Compute internal amounts using LMSR (exact-input with price limit) (amountInInternalUsed, amountOutInternal) = _swapAmountsForExactInput(inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice); // Convert actual used input internal -> uint (ceil) amountInUintNoFee = _internalToUintCeil(amountInInternalUsed, bases[inputTokenIndex]); require(amountInUintNoFee > 0, "swap: input zero"); // Compute gross transfer including fee on the used input (ceil) feeUint = 0; grossIn = amountInUintNoFee; if (SWAP_FEE_PPM > 0) { feeUint = _ceilFee(amountInUintNoFee, SWAP_FEE_PPM); grossIn += feeUint; } // Ensure within user max require(grossIn <= maxAmountIn, "swap: transfer exceeds max"); // Compute output (floor) amountOutUint = _internalToUintFloor(amountOutInternal, bases[outputTokenIndex]); require(amountOutUint > 0, "swap: output zero"); } /// @inheritdoc IPartyPool function swapToLimit( address payer, address receiver, uint256 inputTokenIndex, uint256 outputTokenIndex, int128 limitPrice, uint256 deadline ) external nonReentrant returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) { bytes memory data = abi.encodeWithSignature( 'swapToLimit(address,address,uint256,uint256,int128,uint256,uint256,uint256)', payer, receiver, inputTokenIndex, outputTokenIndex, limitPrice, deadline, SWAP_FEE_PPM, PROTOCOL_FEE_PPM ); bytes memory result = Address.functionDelegateCall(address(SWAP_IMPL), data); return abi.decode(result, (uint256,uint256,uint256)); } /// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP. /// @dev This function forwards the call to the swapMint implementation via delegatecall /// @param payer who transfers the input token /// @param receiver who receives the minted LP tokens /// @param inputTokenIndex index of the input token /// @param maxAmountIn maximum uint token input (inclusive of fee) /// @param deadline optional deadline /// @return lpMinted actual LP minted (uint) function swapMint( address payer, address receiver, uint256 inputTokenIndex, uint256 maxAmountIn, uint256 deadline ) external returns (uint256 lpMinted) { bytes memory data = abi.encodeWithSignature( "swapMint(address,address,uint256,uint256,uint256,uint256,uint256)", payer, receiver, inputTokenIndex, maxAmountIn, deadline, SWAP_FEE_PPM, PROTOCOL_FEE_PPM ); bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data); return abi.decode(result, (uint256)); } /// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver. /// @dev This function forwards the call to the burnSwap implementation via delegatecall /// @param payer who burns LP tokens /// @param receiver who receives the single asset /// @param lpAmount amount of LP tokens to burn /// @param inputTokenIndex index of target asset to receive /// @param deadline optional deadline /// @return amountOutUint uint amount of asset i sent to receiver function burnSwap( address payer, address receiver, uint256 lpAmount, uint256 inputTokenIndex, uint256 deadline ) external returns (uint256 amountOutUint) { bytes memory data = abi.encodeWithSignature( "burnSwap(address,address,uint256,uint256,uint256,uint256,uint256)", payer, receiver, lpAmount, inputTokenIndex, deadline, SWAP_FEE_PPM, PROTOCOL_FEE_PPM ); bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data); return abi.decode(result, (uint256)); } bytes32 internal constant FLASH_CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); /** * @dev Loan `amount` tokens to `receiver`, and takes it back plus a `flashFee` after the callback. * @param receiver The contract receiving the tokens, needs to implement the `onFlashLoan(address user, uint256 amount, uint256 fee, bytes calldata)` interface. * @param tokenAddr The loan currency. * @param amount The amount of tokens lent. * @param data A data parameter to be passed on to the `receiver` for any custom use. */ function flashLoan( IERC3156FlashBorrower receiver, address tokenAddr, uint256 amount, bytes calldata data ) external nonReentrant returns (bool) { IERC20 token = IERC20(tokenAddr); require(amount <= token.balanceOf(address(this))); (uint256 fee, ) = _computeFee(amount, FLASH_FEE_PPM); require( token.transfer(address(receiver), amount), "FlashLender: Transfer failed" ); require( receiver.onFlashLoan(msg.sender, address(token), amount, fee, data) == FLASH_CALLBACK_SUCCESS, "FlashLender: Callback failed" ); require( token.transferFrom(address(receiver), address(this), amount + fee), "FlashLender: Repay failed" ); return true; } /// @notice Transfer all protocol fees to the configured protocolFeeAddress and zero the ledger. /// @dev Anyone can call; must have protocolFeeAddress != address(0) to be operational. function collectProtocolFees() external nonReentrant { address dest = PROTOCOL_FEE_ADDRESS; require(dest != address(0), "collect: zero addr"); uint256 n = tokens.length; for (uint256 i = 0; i < n; i++) { uint256 owed = protocolFeesOwed[i]; if (owed == 0) continue; uint256 bal = IERC20(tokens[i]).balanceOf(address(this)); require(bal >= owed, "collect: fee > bal"); protocolFeesOwed[i] = 0; // transfer owed tokens to protocol destination tokens[i].safeTransfer(dest, owed); // update cached to effective onchain minus owed cachedUintBalances[i] = bal - owed; } } function _swapAmountsForExactInput(uint256 i, uint256 j, int128 a, int128 limitPrice) internal virtual view returns (int128 amountIn, int128 amountOut) { return lmsr.swapAmountsForExactInput(i, j, a, limitPrice); } /// @notice Compute fee and net amounts for a gross input (fee rounded up to favor the pool). /// @return feeUint fee taken (uint) and netUint remaining for protocol use (uint) function _computeFee(uint256 gross) internal view returns (uint256 feeUint, uint256 netUint) { return _computeFee(gross, SWAP_FEE_PPM); } /// @notice Convenience: return gross = net + fee(net) using ceiling for fee. function _addFee(uint256 netUint) internal view returns (uint256 gross) { return _addFee(netUint, SWAP_FEE_PPM); } }