Files
lmsr-amm/src/PartyPool.sol
2025-09-29 17:07:55 -04:00

291 lines
14 KiB
Solidity

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "./PoolLib.sol";
import "./IPartyPool.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 uses PoolLib for all implementation logic and maintains state in a PoolLib.State struct
contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
using PoolLib for PoolLib.State;
/// @notice Pool state containing all storage variables
PoolLib.State internal s;
/// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q)
/// @dev Pool is constructed with a fixed κ. Clients may use LMSRStabilized.computeKappaFromSlippage(...) to
/// derive κ and pass it here.
int128 public immutable kappa; // kappa in Q64.64
/// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations.
uint256 public immutable swapFeePpm;
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
uint256 public immutable flashFeePpm;
/// @notice If true and there are exactly two assets, an optimized 2-asset stable-pair path is used for some computations.
bool private immutable _stablePair; // if true, the optimized LMSRStabilizedBalancedPair optimization path is enabled
/// @inheritdoc IPartyPool
function tokens(uint256 i) external view returns (IERC20) { return s.tokens[i]; }
/// @inheritdoc IPartyPool
function numTokens() external view returns (uint256) { return s.tokens.length; }
/// @inheritdoc IPartyPool
function allTokens() external view returns (IERC20[] memory) { return s.tokens; }
/// @inheritdoc IPartyPool
function denominators() external view returns (uint256[] memory) { return s.bases; }
/// @notice Mapping from token address => (index+1). A zero value indicates the token is not in the pool.
/// @dev Use index = tokenAddressToIndexPlusOne[token] - 1 when non-zero.
function tokenAddressToIndexPlusOne(IERC20 token) external view returns (uint256) {
return s.tokenAddressToIndexPlusOne[token];
}
/// @notice Scale factor used when converting LMSR Q64.64 totals to LP token units (uint).
/// @dev LP tokens are minted in units equal to ABDK.mulu(lastTotalQ64x64, LP_SCALE).
uint256 public constant LP_SCALE = 1e18; // Scale used to convert LMSR lastTotal (Q64.64) into LP token units (uint)
/// @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 stable_ if true and assets.length==2, then the optimization for 2-asset stablecoin pools is activated.
constructor(
string memory name_,
string memory symbol_,
IERC20[] memory tokens_,
uint256[] memory bases_,
int128 kappa_,
uint256 swapFeePpm_,
uint256 flashFeePpm_,
bool stable_
) ERC20(name_, symbol_) {
kappa = kappa_;
require(swapFeePpm_ < 1_000_000, "Pool: fee >= ppm");
swapFeePpm = swapFeePpm_;
require(flashFeePpm_ < 1_000_000, "Pool: flash fee >= ppm");
flashFeePpm = flashFeePpm_;
_stablePair = stable_ && tokens_.length == 2;
// Initialize state using library
s.initialize(tokens_, bases_);
}
/* ----------------------
Initialization / Mint / Burn (LP token managed)
---------------------- */
/// @inheritdoc IPartyPool
function mintDepositAmounts(uint256 lpTokenAmount) public view returns (uint256[] memory depositAmounts) {
return s.mintDepositAmounts(lpTokenAmount, totalSupply());
}
/// @notice Initial mint to set up pool for the first time.
/// @dev Assumes tokens have already been transferred to the pool prior to calling.
/// Can only be called when the pool is uninitialized (totalSupply() == 0 or lmsr.nAssets == 0).
/// @param receiver address that receives the LP tokens
/// @param lpTokens The number of LP tokens to issue for this mint. If 0, then the number of tokens returned will equal the LMSR internal q total
function initialMint(address receiver, uint256 lpTokens) external nonReentrant
returns (uint256 lpMinted) {
lpMinted = s.initialMint(receiver, lpTokens, kappa, totalSupply());
_mint(receiver, lpMinted);
}
/// @notice Proportional mint for existing pool.
/// @dev Payer must approve the required token amounts before calling.
/// Can only be called when pool is already initialized (totalSupply() > 0 and lmsr.nAssets > 0).
/// Rounds follow the pool-favorable conventions documented in helpers (ceil inputs, floor outputs).
/// @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) {
lpMinted = s.mint(payer, receiver, lpTokenAmount, deadline, totalSupply());
_mint(receiver, lpMinted);
}
/// @inheritdoc IPartyPool
function burnReceiveAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts) {
return s.burnReceiveAmounts(lpTokenAmount, totalSupply());
}
/// @notice Burn LP tokens and withdraw the proportional basket to receiver.
/// @dev Payer must own or approve the LP tokens being burned. The function updates LMSR state
/// proportionally to reflect the reduced pool size after the withdrawal.
/// @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 {
uint256[] memory withdrawAmounts = s.burn(payer, receiver, lpAmount, deadline, totalSupply(), balanceOf(payer));
// Handle LP token burning with allowance
if (msg.sender != payer) {
uint256 allowed = allowance(payer, msg.sender);
require(allowed >= lpAmount, "burn: allowance insufficient");
_approve(payer, msg.sender, allowed - lpAmount);
}
_burn(payer, lpAmount);
}
/* ----------------------
Swaps
---------------------- */
/// @inheritdoc IPartyPool
function swapAmounts(
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
return s.swapAmounts(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm, _stablePair);
}
/// @inheritdoc IPartyPool
function swapToLimitAmounts(
uint256 inputTokenIndex,
uint256 outputTokenIndex,
int128 limitPrice
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
return s.swapToLimitAmounts(inputTokenIndex, outputTokenIndex, limitPrice, swapFeePpm);
}
/// @notice Swap input token i -> token j. Payer must approve token i.
/// @dev This function transfers the exact gross input (including fee) from payer and sends the computed output to receiver.
/// Non-standard tokens (fee-on-transfer, rebasers) are rejected via balance checks.
/// @param payer address of the account that pays for the swap
/// @param receiver address that will receive the output tokens
/// @param inputTokenIndex index of input asset
/// @param outputTokenIndex index of output asset
/// @param maxAmountIn maximum amount of token i (uint256) to transfer in (inclusive of fees)
/// @param limitPrice maximum acceptable marginal price (64.64 fixed point). Pass 0 to ignore.
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
/// @return amountIn actual input used (uint256), amountOut actual output sent (uint256), fee fee taken from the input (uint256)
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) {
return s.swap(payer, receiver, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, deadline, swapFeePpm, _stablePair);
}
/// @notice Swap up to the price limit; computes max input to reach limit then performs swap.
/// @dev If balances prevent fully reaching the limit, the function caps and returns actuals.
/// The payer must transfer the exact gross input computed by the view.
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
function swapToLimit(
address payer,
address receiver,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
int128 limitPrice,
uint256 deadline
) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) {
return s.swapToLimit(payer, receiver, inputTokenIndex, outputTokenIndex, limitPrice, deadline, swapFeePpm);
}
/// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP.
/// @dev swapMint executes as an exact-in planned swap followed by proportional scaling of qInternal.
/// The function emits SwapMint (gross, net, fee) and also emits Mint for LP issuance.
/// @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 nonReentrant returns (uint256 lpMinted) {
lpMinted = s.swapMint(payer, receiver, inputTokenIndex, maxAmountIn, deadline, swapFeePpm, totalSupply());
_mint(receiver, lpMinted);
}
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver.
/// @dev The function burns LP tokens (authorization via allowance if needed), sends the single-asset payout and updates LMSR state.
/// @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 nonReentrant returns (uint256 amountOutUint) {
amountOutUint = s.burnSwap(payer, receiver, lpAmount, inputTokenIndex, deadline, swapFeePpm, totalSupply(), balanceOf(payer));
// Handle LP token burning with allowance
if (msg.sender != payer) {
uint256 allowed = allowance(payer, msg.sender);
require(allowed >= lpAmount, "burnSwap: allowance insufficient");
_approve(payer, msg.sender, allowed - lpAmount);
}
_burn(payer, lpAmount);
}
/// @inheritdoc IPartyPool
function flashRepaymentAmounts(uint256[] memory loanAmounts) external view
returns (uint256[] memory repaymentAmounts) {
return s.flashRepaymentAmounts(loanAmounts, flashFeePpm);
}
/// @notice Receive token amounts and require them to be repaid plus a fee inside a callback.
/// @dev The caller must implement IPartyFlashCallback#partyFlashCallback which receives (amounts, repaymentAmounts, data).
/// This function verifies that, after the callback returns, the pool's balances have increased by at least the fees
/// for each borrowed token. Reverts if repayment (including fee) did not occur.
/// @param recipient The address which will receive the token amounts
/// @param amounts The amount of each token to send (array length must equal pool size)
/// @param data Any data to be passed through to the callback
function flash(
address recipient,
uint256[] memory amounts,
bytes calldata data
) external nonReentrant {
s.flash(recipient, amounts, data, flashFeePpm);
}
/// @notice Marginal price of `base` in terms of `quote` (p_quote / p_base) as Q64.64
/// @dev Returns the LMSR marginal price directly (raw 64.64) for use by off-chain quoting logic.
function price(uint256 baseTokenIndex, uint256 quoteTokenIndex) external view returns (int128) {
return s.price(baseTokenIndex, quoteTokenIndex);
}
/// @notice Price of one LP token denominated in `quote` asset as Q64.64
/// @dev Computes LMSR poolPrice (quote per unit qTotal) and scales it by totalSupply() / qTotal
/// to return price per LP token unit in quote asset (raw 64.64).
function poolPrice(uint256 quoteTokenIndex) external view returns (int128) {
return s.poolPrice(quoteTokenIndex, totalSupply());
}
}