This commit is contained in:
tim
2025-09-29 17:07:55 -04:00
parent 77683555e8
commit 8e69bfac5c
2 changed files with 878 additions and 709 deletions

View File

@@ -1,15 +1,11 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import "@abdk/ABDKMath64x64.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "./LMSRStabilized.sol";
import "./LMSRStabilizedBalancedPair.sol";
import "./PoolLib.sol";
import "./IPartyPool.sol";
import "./IPartyFlashCallback.sol";
/// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token
/// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model.
@@ -20,36 +16,12 @@ import "./IPartyFlashCallback.sol";
/// - 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).
/// @dev The contract uses PoolLib for all implementation logic and maintains state in a PoolLib.State struct
contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
using ABDKMath64x64 for int128;
using LMSRStabilized for LMSRStabilized.State;
using SafeERC20 for IERC20;
using PoolLib for PoolLib.State;
/// @notice Token addresses comprising the pool. Effectively immutable after construction.
/// @dev tokens[i] corresponds to the i-th asset and maps to index i in the internal LMSR arrays.
IERC20[] public tokens; // effectively immutable since there is no interface to change the tokens
LMSRStabilized.State internal lmsr;
// Cached on-chain balances (uint) and internal 64.64 representation
// balance / base = internal
uint256[] internal cachedUintBalances;
/// @notice Per-token uint base denominators used to convert uint token amounts <-> internal Q64.64 representation.
/// @dev denominators()[i] is the base for tokens[i]. These bases are chosen by deployer and must match token decimals.
uint256[] internal bases; // per-token uint base used to scale token amounts <-> internal
/// @inheritdoc IPartyPool
function numTokens() external view returns (uint256) { return tokens.length; }
/// @inheritdoc IPartyPool
function allTokens() external view returns (IERC20[] memory) { return tokens; }
/// @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
@@ -67,11 +39,22 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
/// @inheritdoc IPartyPool
function denominators() external view returns (uint256[] memory) { return bases; }
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.
mapping(IERC20=>uint) public tokenAddressToIndexPlusOne; // Uses index+1 so a result of 0 indicates a failed lookup
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).
@@ -95,10 +78,6 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
uint256 flashFeePpm_,
bool stable_
) ERC20(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");
swapFeePpm = swapFeePpm_;
@@ -106,50 +85,17 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
flashFeePpm = flashFeePpm_;
_stablePair = stable_ && tokens_.length == 2;
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
cachedUintBalances = new uint256[](n);
// 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) {
uint256 n = tokens.length;
depositAmounts = new uint256[](n);
// If this is the first mint or pool is empty, return zeros
// For first mint, tokens should already be transferred to the pool
if (totalSupply() == 0 || lmsr.nAssets == 0) {
return depositAmounts; // Return zeros, initial deposit handled differently
}
// Calculate deposit based on current proportions
uint256 totalLpSupply = totalSupply();
// lpTokenAmount / totalLpSupply = depositAmount / currentBalance
// Therefore: depositAmount = (lpTokenAmount * currentBalance) / totalLpSupply
// We round up to protect the pool
for (uint i = 0; i < n; i++) {
uint256 currentBalance = cachedUintBalances[i];
// Calculate with rounding up: (a * b + c - 1) / c
depositAmounts[i] = (lpTokenAmount * currentBalance + totalLpSupply - 1) / totalLpSupply;
}
return depositAmounts;
return s.mintDepositAmounts(lpTokenAmount, totalSupply());
}
/// @notice Initial mint to set up pool for the first time.
@@ -159,37 +105,8 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
/// @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) {
uint256 n = tokens.length;
// Check if this is initial deposit - revert if not
bool isInitialDeposit = totalSupply() == 0 || lmsr.nAssets == 0;
require(isInitialDeposit, "initialMint: pool already initialized");
// Update cached balances for all assets
int128[] memory newQInternal = new int128[](n);
uint256[] memory depositAmounts = new uint256[](n);
for (uint i = 0; i < n; ) {
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
cachedUintBalances[i] = bal;
newQInternal[i] = _uintToInternalFloor(bal, bases[i]);
depositAmounts[i] = bal;
unchecked { i++; }
}
// Initialize the stabilized LMSR state with provided kappa
lmsr.init(newQInternal, kappa);
// Compute actual LP tokens to mint based on size metric (scaled)
if( lpTokens != 0 )
lpMinted = lpTokens;
else {
int128 newTotal = _computeSizeMetric(newQInternal);
lpMinted = ABDKMath64x64.mulu(newTotal, LP_SCALE);
}
require(lpMinted > 0, "initialMint: zero LP amount");
lpMinted = s.initialMint(receiver, lpTokens, kappa, totalSupply());
_mint(receiver, lpMinted);
emit Mint(address(0), receiver, depositAmounts, lpMinted);
}
/// @notice Proportional mint for existing pool.
@@ -202,94 +119,13 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
/// @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) {
require(deadline == 0 || block.timestamp <= deadline, "mint: deadline exceeded");
uint256 n = tokens.length;
// Check if this is NOT initial deposit - revert if it is
bool isInitialDeposit = totalSupply() == 0 || lmsr.nAssets == 0;
require(!isInitialDeposit, "mint: use initialMint for pool initialization");
require(lpTokenAmount > 0, "mint: zero LP amount");
// Capture old pool size metric (scaled) by computing from current balances
int128 oldTotal = _computeSizeMetric(lmsr.qInternal);
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
// Calculate required deposit amounts for the desired LP tokens
uint256[] memory depositAmounts = mintDepositAmounts(lpTokenAmount);
// Transfer in all token amounts
for (uint i = 0; i < n; ) {
if (depositAmounts[i] > 0) {
tokens[i].safeTransferFrom(payer, address(this), depositAmounts[i]);
}
unchecked { i++; }
}
// Update cached balances for all assets
int128[] memory newQInternal = new int128[](n);
for (uint i = 0; i < n; ) {
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
cachedUintBalances[i] = bal;
newQInternal[i] = _uintToInternalFloor(bal, bases[i]);
unchecked { i++; }
}
// Update for proportional change
lmsr.updateForProportionalChange(newQInternal);
// Compute actual LP tokens to mint based on change in size metric (scaled)
// floor truncation rounds in favor of the pool
int128 newTotal = _computeSizeMetric(newQInternal);
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
uint256 actualLpToMint;
require(oldScaled > 0, "mint: oldScaled zero");
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
// Proportional issuance: totalSupply * delta / oldScaled
if (delta > 0) {
// floor truncation rounds in favor of the pool
actualLpToMint = (totalSupply() * delta) / oldScaled;
} else {
actualLpToMint = 0;
}
// Ensure the calculated LP amount is not too different from requested
require(actualLpToMint > 0, "mint: zero LP minted");
// Allow actual amount to be at most 0.00001% less than requested
// This accounts for rounding in deposit calculations
uint256 minAcceptable = lpTokenAmount * 99_999 / 100_000;
require(actualLpToMint >= minAcceptable, "mint: insufficient LP minted");
_mint(receiver, actualLpToMint);
emit Mint(payer, receiver, depositAmounts, actualLpToMint);
return actualLpToMint;
lpMinted = s.mint(payer, receiver, lpTokenAmount, deadline, totalSupply());
_mint(receiver, lpMinted);
}
/// @inheritdoc IPartyPool
function burnReceiveAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts) {
return _burnReceiveAmounts(lpTokenAmount);
}
function _burnReceiveAmounts(uint256 lpTokenAmount) internal view returns (uint256[] memory withdrawAmounts) {
uint256 n = tokens.length;
withdrawAmounts = new uint256[](n);
// If supply is zero or pool uninitialized, return zeros
if (totalSupply() == 0 || lmsr.nAssets == 0) {
return withdrawAmounts; // Return zeros, nothing to withdraw
}
// Calculate withdrawal amounts based on current proportions
uint256 totalLpSupply = totalSupply();
// withdrawAmount = floor(lpTokenAmount * currentBalance / totalLpSupply)
for (uint i = 0; i < n; i++) {
uint256 currentBalance = cachedUintBalances[i];
withdrawAmounts[i] = (lpTokenAmount * currentBalance) / totalLpSupply;
}
return withdrawAmounts;
return s.burnReceiveAmounts(lpTokenAmount, totalSupply());
}
/// @notice Burn LP tokens and withdraw the proportional basket to receiver.
@@ -300,67 +136,15 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
/// @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 {
require(deadline == 0 || block.timestamp <= deadline, "burn: deadline exceeded");
uint256 n = tokens.length;
require(lpAmount > 0, "burn: zero lp");
uint256[] memory withdrawAmounts = s.burn(payer, receiver, lpAmount, deadline, totalSupply(), balanceOf(payer));
uint256 supply = totalSupply();
require(supply > 0, "burn: empty supply");
require(lmsr.nAssets > 0, "burn: uninit pool");
require(balanceOf(payer) >= lpAmount, "burn: insufficient LP");
// Refresh cached balances to reflect current on-chain balances before computing withdrawal amounts
for (uint i = 0; i < n; ) {
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
cachedUintBalances[i] = bal;
unchecked { i++; }
}
// Compute proportional withdrawal amounts for the requested LP amount (rounded down)
uint256[] memory withdrawAmounts = _burnReceiveAmounts(lpAmount);
// Transfer underlying tokens out to receiver according to computed proportions
for (uint i = 0; i < n; ) {
if (withdrawAmounts[i] > 0) {
tokens[i].safeTransfer(receiver, withdrawAmounts[i]);
}
unchecked { i++; }
}
// Update cached balances and internal q for all assets
int128[] memory newQInternal = new int128[](n);
for (uint i = 0; i < n; ) {
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
cachedUintBalances[i] = bal;
newQInternal[i] = _uintToInternalFloor(bal, bases[i]);
unchecked { i++; }
}
// Apply proportional update or deinitialize if drained
bool allZero = true;
for (uint i = 0; i < n; ) {
if (newQInternal[i] != int128(0)) {
allZero = false;
break;
}
unchecked { i++; }
}
if (allZero) {
lmsr.deinit();
} else {
lmsr.updateForProportionalChange(newQInternal);
}
// Burn exactly the requested LP amount from payer (authorization via allowance)
// 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);
emit Burn(payer, receiver, withdrawAmounts, lpAmount);
}
/* ----------------------
@@ -374,8 +158,7 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
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);
return s.swapAmounts(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm, _stablePair);
}
/// @inheritdoc IPartyPool
@@ -384,11 +167,9 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
uint256 outputTokenIndex,
int128 limitPrice
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapToLimit(inputTokenIndex, outputTokenIndex, limitPrice);
return (grossIn, outUint, feeUint);
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.
@@ -409,39 +190,7 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
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");
// Update cached uint balances for i and j using actual balances
cachedUintBalances[inputTokenIndex] = balIAfter;
cachedUintBalances[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);
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.
@@ -456,175 +205,9 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
int128 limitPrice,
uint256 deadline
) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) {
uint256 n = tokens.length;
require(inputTokenIndex < n && outputTokenIndex < n, "swapToLimit: idx");
require(limitPrice > int128(0), "swapToLimit: limit <= 0");
require(deadline == 0 || block.timestamp <= deadline, "swapToLimit: 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 amountInInternalMax, int128 amountOutInternal, uint256 amountInUsedUint, uint256 feeUint) =
_quoteSwapToLimit(inputTokenIndex, outputTokenIndex, limitPrice);
// Transfer the exact amount needed 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, "swapToLimit: 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, "swapToLimit: non-standard tokenOut");
// Update caches to actual balances
cachedUintBalances[inputTokenIndex] = balIAfter;
cachedUintBalances[outputTokenIndex] = balJAfter;
// Apply swap to LMSR state with the internal amounts
lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalMax, amountOutInternal);
// Maintain original event semantics (logs input without fee)
emit Swap(payer, receiver, tokens[inputTokenIndex], tokens[outputTokenIndex], amountInUsedUint, amountOutUint);
return (amountInUsedUint, amountOutUint, feeUint);
return s.swapToLimit(payer, receiver, inputTokenIndex, outputTokenIndex, limitPrice, deadline, swapFeePpm);
}
/// @notice Ceiling fee helper: computes ceil(x * feePpm / 1_000_000)
/// @dev Internal helper; public-facing functions use this to ensure fees round up in favor of pool.
function _ceilFee(uint256 x, uint256 feePpm) internal pure returns (uint256) {
if (feePpm == 0) return 0;
// ceil division: (num + denom - 1) / denom
return (x * feePpm + 1_000_000 - 1) / 1_000_000;
}
/// @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);
// 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)
// if _stablePair is true, use the optimized path
(amountInInternalUsed, amountOutInternal) =
_stablePair ? LMSRStabilizedBalancedPair.swapAmountsForExactInput(lmsr, inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice)
: lmsr.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 (swapFeePpm > 0) {
feeUint = _ceilFee(amountInUintNoFee, swapFeePpm);
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");
}
/// @notice Internal quote for swap-to-limit that mirrors swapToLimit() rounding and fee application
/// @dev Computes the input required to reach limitPrice and the resulting output; all rounding matches swapToLimit.
/// @return grossIn amount to transfer in (inclusive of fee), amountOutUint output amount (uint),
/// amountInInternal and amountOutInternal (64.64), amountInUintNoFee input amount excluding fee (uint),
/// feeUint fee taken from the gross input (uint)
function _quoteSwapToLimit(
uint256 inputTokenIndex,
uint256 outputTokenIndex,
int128 limitPrice
)
internal
view
returns (
uint256 grossIn,
uint256 amountOutUint,
int128 amountInInternal,
int128 amountOutInternal,
uint256 amountInUintNoFee,
uint256 feeUint
)
{
uint256 n = tokens.length;
require(inputTokenIndex < n && outputTokenIndex < n, "swapToLimit: idx");
require(limitPrice > int128(0), "swapToLimit: limit <= 0");
require(lmsr.nAssets > 0, "swapToLimit: pool uninitialized");
// Compute internal maxima at the price limit
(amountInInternal, amountOutInternal) = lmsr.swapAmountsForPriceLimit(inputTokenIndex, outputTokenIndex, limitPrice);
// Convert input to uint (ceil) and output to uint (floor)
amountInUintNoFee = _internalToUintCeil(amountInInternal, bases[inputTokenIndex]);
require(amountInUintNoFee > 0, "swapToLimit: input zero");
feeUint = 0;
grossIn = amountInUintNoFee;
if (swapFeePpm > 0) {
feeUint = _ceilFee(amountInUintNoFee, swapFeePpm);
grossIn += feeUint;
}
amountOutUint = _internalToUintFloor(amountOutInternal, bases[outputTokenIndex]);
require(amountOutUint > 0, "swapToLimit: output zero");
}
/// @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) {
if (swapFeePpm == 0) {
return (0, gross);
}
feeUint = _ceilFee(gross, swapFeePpm);
netUint = gross - feeUint;
}
/// @notice Convenience: return gross = net + fee(net) using ceiling for fee.
function _addFee(uint256 netUint) internal view returns (uint256 gross) {
if (swapFeePpm == 0) return netUint;
uint256 fee = _ceilFee(netUint, swapFeePpm);
return netUint + fee;
}
// --- New events for single-token mint/burn flows ---
// Note: events intentionally avoid exposing internal ΔS and avoid duplicating LP mint/burn data
// which is already present in the standard Mint/Burn events.
/// @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.
@@ -641,90 +224,8 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
uint256 maxAmountIn,
uint256 deadline
) external nonReentrant returns (uint256 lpMinted) {
uint256 n = tokens.length;
require(inputTokenIndex < n, "swapMint: idx");
require(maxAmountIn > 0, "swapMint: input zero");
require(deadline == 0 || block.timestamp <= deadline, "swapMint: deadline");
// Ensure pool initialized
require(lmsr.nAssets > 0, "swapMint: uninit pool");
// compute fee on gross maxAmountIn to get an initial net estimate (we'll recompute based on actual used)
(, uint256 netUintGuess) = _computeFee(maxAmountIn);
// Convert the net guess to internal (floor)
int128 netInternalGuess = _uintToInternalFloor(netUintGuess, bases[inputTokenIndex]);
require(netInternalGuess > int128(0), "swapMint: input too small after fee");
// Use LMSR view to determine actual internal consumed and size-increase (ΔS) for mint
(int128 amountInInternalUsed, int128 sizeIncreaseInternal) = lmsr.swapAmountsForMint(inputTokenIndex, netInternalGuess);
// amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer
uint256 amountInUint = _internalToUintCeil(amountInInternalUsed, bases[inputTokenIndex]);
require(amountInUint > 0, "swapMint: input zero after internal conversion");
// Compute fee on the actual used input and total transfer amount (ceiling)
uint256 feeUintActual = _ceilFee(amountInUint, swapFeePpm);
uint256 totalTransfer = amountInUint + feeUintActual;
require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMint: transfer exceeds max");
// Record pre-balance and transfer tokens from payer, require exact receipt (revert on fee-on-transfer)
uint256 prevBalI = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransfer);
uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
require(balIAfter == prevBalI + totalTransfer, "swapMint: non-standard tokenIn");
// Update cached uint balances for token inputTokenIndex (only inputTokenIndex changed externally)
cachedUintBalances[inputTokenIndex] = balIAfter;
// Compute old and new scaled size metrics to determine LP minted
int128 oldTotal = _computeSizeMetric(lmsr.qInternal);
require(oldTotal > int128(0), "swapMint: zero total");
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
int128 newTotal = oldTotal.add(sizeIncreaseInternal);
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
uint256 actualLpToMint;
if (totalSupply() == 0) {
// If somehow supply zero (shouldn't happen as lmsr.nAssets>0), mint newScaled
actualLpToMint = newScaled;
} else {
require(oldScaled > 0, "swapMint: oldScaled zero");
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
if (delta > 0) {
// floor truncation rounds in favor of pool
actualLpToMint = (totalSupply() * delta) / oldScaled;
} else {
actualLpToMint = 0;
}
}
require(actualLpToMint > 0, "swapMint: zero LP minted");
// Update LMSR internal state: scale qInternal proportionally by newTotal/oldTotal
int128[] memory newQInternal = new int128[](n);
for (uint256 idx = 0; idx < n; idx++) {
// newQInternal[idx] = qInternal[idx] * (newTotal / oldTotal)
newQInternal[idx] = lmsr.qInternal[idx].mul(newTotal).div(oldTotal);
}
// Update cached internal and kappa via updateForProportionalChange
lmsr.updateForProportionalChange(newQInternal);
// Note: we updated cachedUintBalances[inputTokenIndex] above via reading balance; other token uint balances did not
// change externally (they were not transferred in). We keep cachedUintBalances for others unchanged.
// Mint LP tokens to receiver
_mint(receiver, actualLpToMint);
// Emit SwapMint event with gross transfer, net input and fee (planned exact-in)
emit SwapMint(payer, receiver, inputTokenIndex, totalTransfer, amountInUint, feeUintActual);
// Emit standard Mint event which records deposit amounts and LP minted
emit Mint(payer, receiver, new uint256[](n), actualLpToMint);
// Note: depositAmounts array omitted (empty) since swapMint uses single-token input
return actualLpToMint;
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.
@@ -742,76 +243,23 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
uint256 inputTokenIndex,
uint256 deadline
) external nonReentrant returns (uint256 amountOutUint) {
uint256 n = tokens.length;
require(inputTokenIndex < n, "burnSwap: idx");
require(lpAmount > 0, "burnSwap: zero lp");
require(deadline == 0 || block.timestamp <= deadline, "burnSwap: deadline");
amountOutUint = s.burnSwap(payer, receiver, lpAmount, inputTokenIndex, deadline, swapFeePpm, totalSupply(), balanceOf(payer));
uint256 supply = totalSupply();
require(supply > 0, "burnSwap: empty supply");
require(balanceOf(payer) >= lpAmount, "burnSwap: insufficient LP");
// alpha = lpAmount / supply as Q64.64
int128 alpha = ABDKMath64x64.divu(lpAmount, supply);
// Use LMSR view to compute single-asset payout and burned size-metric
(int128 payoutInternal, ) = lmsr.swapAmountsForBurn(inputTokenIndex, alpha);
// Convert payoutInternal -> uint (floor) to favor pool
amountOutUint = _internalToUintFloor(payoutInternal, bases[inputTokenIndex]);
require(amountOutUint > 0, "burnSwap: output zero");
// Transfer the payout to receiver
tokens[inputTokenIndex].safeTransfer(receiver, amountOutUint);
// Burn LP tokens from payer (authorization via allowance)
// 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);
// Update cached balances by reading on-chain balances for all tokens
int128[] memory newQInternal = new int128[](n);
for (uint256 idx = 0; idx < n; idx++) {
uint256 bal = IERC20(tokens[idx]).balanceOf(address(this));
cachedUintBalances[idx] = bal;
newQInternal[idx] = _uintToInternalFloor(bal, bases[idx]);
}
// Emit BurnSwap with public-facing info only (do not expose ΔS or LP burned)
emit BurnSwap(payer, receiver, inputTokenIndex, amountOutUint);
// If entire pool drained, deinit; else update proportionally
bool allZero = true;
for (uint256 idx = 0; idx < n; idx++) {
if (newQInternal[idx] != int128(0)) { allZero = false; break; }
}
if (allZero) {
lmsr.deinit();
} else {
lmsr.updateForProportionalChange(newQInternal);
}
emit Burn(payer, receiver, new uint256[](n), lpAmount);
return amountOutUint;
}
/// @inheritdoc IPartyPool
function flashRepaymentAmounts(uint256[] memory loanAmounts) external view
returns (uint256[] memory repaymentAmounts) {
repaymentAmounts = new uint256[](tokens.length);
for (uint256 i = 0; i < tokens.length; i++) {
uint256 amount = loanAmounts[i];
if (amount > 0) {
repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm);
}
}
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
@@ -824,137 +272,19 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
uint256[] memory amounts,
bytes calldata data
) external nonReentrant {
require(recipient != address(0), "flash: zero recipient");
require(amounts.length == tokens.length, "flash: amounts length mismatch");
// Calculate repayment amounts for each token including fee
uint256[] memory repaymentAmounts = new uint256[](tokens.length);
// Store initial balances to verify repayment later
uint256[] memory initialBalances = new uint256[](tokens.length);
// Track if any token amount is non-zero
bool hasNonZeroAmount = false;
// Process each token, skipping those with zero amounts
for (uint256 i = 0; i < tokens.length; i++) {
uint256 amount = amounts[i];
if (amount > 0) {
hasNonZeroAmount = true;
// Calculate repayment amount with fee (ceiling)
repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm);
// Record initial balance
initialBalances[i] = IERC20(tokens[i]).balanceOf(address(this));
// Transfer token to recipient
tokens[i].safeTransfer(recipient, amount);
}
}
// Ensure at least one token is being borrowed
require(hasNonZeroAmount, "flash: no tokens requested");
// Call flash callback with expected repayment amounts
IPartyFlashCallback(msg.sender).partyFlashCallback(amounts, repaymentAmounts, data);
// Verify repayment amounts for tokens that were borrowed
for (uint256 i = 0; i < tokens.length; i++) {
if (amounts[i] > 0) {
uint256 currentBalance = IERC20(tokens[i]).balanceOf(address(this));
// Verify repayment: current balance must be at least (initial balance + fee)
require(
currentBalance >= initialBalances[i] + _ceilFee(amounts[i], flashFeePpm),
"flash: repayment failed"
);
// Update cached balance
cachedUintBalances[i] = currentBalance;
}
}
}
/* ----------------------
Conversion helpers
---------------------- */
// Convert uint token amount -> internal 64.64 (floor). Uses ABDKMath64x64.divu which truncates.
function _uintToInternalFloor(uint256 amount, uint256 base) internal pure returns (int128) {
// internal = amount / base (as Q64.64)
return ABDKMath64x64.divu(amount, base);
}
// Convert internal 64.64 -> uint token amount (floor). Uses ABDKMath64x64.mulu which floors the product.
function _internalToUintFloor(int128 internalAmount, uint256 base) internal pure returns (uint256) {
// uint = internal * base (floored)
return ABDKMath64x64.mulu(internalAmount, base);
}
// Convert internal 64.64 -> uint token amount (ceiling). Rounds up to protect the pool.
function _internalToUintCeil(int128 internalAmount, uint256 base) internal pure returns (uint256) {
// Get the floor value first
uint256 floorValue = ABDKMath64x64.mulu(internalAmount, base);
// Check if there was any fractional part by comparing to a reconstruction of the original
int128 reconstructed = ABDKMath64x64.divu(floorValue, base);
// If reconstructed is less than original, there was a fractional part that was truncated
if (reconstructed < internalAmount) {
return floorValue + 1;
}
return floorValue;
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) {
uint256 n = tokens.length;
require(baseTokenIndex < n && quoteTokenIndex < n, "price: idx");
require(lmsr.nAssets > 0, "price: uninit");
return lmsr.price(baseTokenIndex, quoteTokenIndex);
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) {
uint256 n = tokens.length;
require(quoteTokenIndex < n, "poolPrice: idx");
require(lmsr.nAssets > 0, "poolPrice: uninit");
// price per unit of qTotal (Q64.64) from LMSR
int128 pricePerQ = lmsr.poolPrice(quoteTokenIndex);
// total internal q (qTotal) as Q64.64
int128 qTotal = _computeSizeMetric(lmsr.qInternal);
require(qTotal > int128(0), "poolPrice: qTotal zero");
// totalSupply as Q64.64
uint256 supply = totalSupply();
require(supply > 0, "poolPrice: zero supply");
int128 supplyQ64 = ABDKMath64x64.fromUInt(supply);
// factor = totalSupply / qTotal (Q64.64)
int128 factor = supplyQ64.div(qTotal);
// price per LP token = pricePerQ * factor (Q64.64)
return pricePerQ.mul(factor);
return s.poolPrice(quoteTokenIndex, totalSupply());
}
/// @notice Helper to compute size metric (sum of all asset quantities) from internal balances
/// @dev Returns the sum of all provided qInternal_ entries as a Q64.64 value.
function _computeSizeMetric(int128[] memory qInternal_) private pure returns (int128) {
int128 total = int128(0);
for (uint i = 0; i < qInternal_.length; ) {
total = total.add(qInternal_[i]);
unchecked { i++; }
}
return total;
}
}