291 lines
14 KiB
Solidity
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());
|
|
}
|
|
}
|