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;
}
}

839
src/PoolLib.sol Normal file
View File

@@ -0,0 +1,839 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import "@abdk/ABDKMath64x64.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "./LMSRStabilized.sol";
import "./LMSRStabilizedBalancedPair.sol";
import "./IPartyFlashCallback.sol";
/// @title PoolLib - Library containing all PartyPool implementation logic
/// @notice This library contains the core implementation for LMSR-backed multi-asset pools
/// @dev All functions are internal and accept State as the first parameter
library PoolLib {
using ABDKMath64x64 for int128;
using LMSRStabilized for LMSRStabilized.State;
using SafeERC20 for IERC20;
/// @notice State struct containing all storage variables from PartyPool
/// @dev This struct is passed to all library functions as the first parameter
struct State {
/// @notice Token addresses comprising the pool
IERC20[] tokens;
/// @notice LMSR state for pricing computations
LMSRStabilized.State lmsr;
/// @notice Cached on-chain balances (uint) for each token
uint256[] cachedUintBalances;
/// @notice Per-token uint base denominators used to convert uint <-> internal Q64.64
uint256[] bases;
/// @notice Mapping from token address => (index+1). Zero means token not in pool.
mapping(IERC20=>uint) tokenAddressToIndexPlusOne;
}
/// @notice Scale factor used when converting LMSR Q64.64 totals to LP token units (uint)
uint256 internal constant LP_SCALE = 1e18;
// Events that mirror the main contract events
event Mint(address indexed payer, address indexed receiver, uint256[] depositAmounts, uint256 lpMinted);
event Burn(address indexed payer, address indexed receiver, uint256[] withdrawAmounts, uint256 lpBurned);
event Swap(address indexed payer, address indexed receiver, IERC20 indexed tokenIn, IERC20 tokenOut, uint256 amountIn, uint256 amountOut);
event SwapMint(address indexed payer, address indexed receiver, uint256 indexed inputTokenIndex, uint256 totalTransfer, uint256 amountInUint, uint256 feeUintActual);
event BurnSwap(address indexed payer, address indexed receiver, uint256 indexed inputTokenIndex, uint256 amountOutUint);
/// @notice Initialize the pool state with tokens and bases
/// @param state The pool state
/// @param tokens_ Array of token addresses
/// @param bases_ Array of base denominators for each token
function initialize(
State storage state,
IERC20[] memory tokens_,
uint256[] memory bases_
) internal {
require(tokens_.length > 1, "Pool: need >1 asset");
require(tokens_.length == bases_.length, "Pool: lengths mismatch");
state.tokens = tokens_;
state.bases = bases_;
uint256 n = tokens_.length;
// Initialize LMSR state nAssets; full init occurs on first mint
state.lmsr.nAssets = n;
// Initialize token address to index mapping
for (uint i = 0; i < n;) {
state.tokenAddressToIndexPlusOne[tokens_[i]] = i + 1;
unchecked {i++;}
}
// Initialize caches to zero
state.cachedUintBalances = new uint256[](n);
}
/// @notice Get deposit amounts needed for minting LP tokens
function mintDepositAmounts(
State storage state,
uint256 lpTokenAmount,
uint256 totalSupply
) internal view returns (uint256[] memory depositAmounts) {
uint256 n = state.tokens.length;
depositAmounts = new uint256[](n);
// If this is the first mint or pool is empty, return zeros
if (totalSupply == 0 || state.lmsr.nAssets == 0) {
return depositAmounts;
}
// Calculate deposit based on current proportions
for (uint i = 0; i < n; i++) {
uint256 currentBalance = state.cachedUintBalances[i];
// Calculate with rounding up: (a * b + c - 1) / c
depositAmounts[i] = (lpTokenAmount * currentBalance + totalSupply - 1) / totalSupply;
}
return depositAmounts;
}
/// @notice Initial mint to set up pool for the first time
function initialMint(
State storage state,
address receiver,
uint256 lpTokens,
int128 kappa,
uint256 totalSupply
) internal returns (uint256 lpMinted) {
uint256 n = state.tokens.length;
// Check if this is initial deposit
bool isInitialDeposit = totalSupply == 0 || state.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(state.tokens[i]).balanceOf(address(this));
state.cachedUintBalances[i] = bal;
newQInternal[i] = _uintToInternalFloor(bal, state.bases[i]);
depositAmounts[i] = bal;
unchecked { i++; }
}
// Initialize the stabilized LMSR state with provided kappa
state.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");
emit Mint(address(0), receiver, depositAmounts, lpMinted);
}
/// @notice Proportional mint for existing pool
function mint(
State storage state,
address payer,
address receiver,
uint256 lpTokenAmount,
uint256 deadline,
uint256 totalSupply
) internal returns (uint256 lpMinted) {
require(deadline == 0 || block.timestamp <= deadline, "mint: deadline exceeded");
uint256 n = state.tokens.length;
// Check if this is NOT initial deposit
bool isInitialDeposit = totalSupply == 0 || state.lmsr.nAssets == 0;
require(!isInitialDeposit, "mint: use initialMint for pool initialization");
require(lpTokenAmount > 0, "mint: zero LP amount");
// Capture old pool size metric (scaled)
int128 oldTotal = _computeSizeMetric(state.lmsr.qInternal);
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
// Calculate required deposit amounts
uint256[] memory depositAmounts = mintDepositAmounts(state, lpTokenAmount, totalSupply);
// Transfer in all token amounts
for (uint i = 0; i < n; ) {
if (depositAmounts[i] > 0) {
state.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(state.tokens[i]).balanceOf(address(this));
state.cachedUintBalances[i] = bal;
newQInternal[i] = _uintToInternalFloor(bal, state.bases[i]);
unchecked { i++; }
}
// Update for proportional change
state.lmsr.updateForProportionalChange(newQInternal);
// Compute actual LP tokens to mint
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;
if (delta > 0) {
actualLpToMint = (totalSupply * delta) / oldScaled;
} else {
actualLpToMint = 0;
}
require(actualLpToMint > 0, "mint: zero LP minted");
// Allow actual amount to be at most 0.00001% less than requested
uint256 minAcceptable = lpTokenAmount * 99_999 / 100_000;
require(actualLpToMint >= minAcceptable, "mint: insufficient LP minted");
emit Mint(payer, receiver, depositAmounts, actualLpToMint);
return actualLpToMint;
}
/// @notice Get withdrawal amounts for burning LP tokens
function burnReceiveAmounts(
State storage state,
uint256 lpTokenAmount,
uint256 totalSupply
) internal view returns (uint256[] memory withdrawAmounts) {
uint256 n = state.tokens.length;
withdrawAmounts = new uint256[](n);
if (totalSupply == 0 || state.lmsr.nAssets == 0) {
return withdrawAmounts;
}
for (uint i = 0; i < n; i++) {
uint256 currentBalance = state.cachedUintBalances[i];
withdrawAmounts[i] = (lpTokenAmount * currentBalance) / totalSupply;
}
return withdrawAmounts;
}
/// @notice Burn LP tokens and withdraw proportional basket
function burn(
State storage state,
address payer,
address receiver,
uint256 lpAmount,
uint256 deadline,
uint256 totalSupply,
uint256 payerBalance
) internal returns (uint256[] memory withdrawAmounts) {
require(deadline == 0 || block.timestamp <= deadline, "burn: deadline exceeded");
uint256 n = state.tokens.length;
require(lpAmount > 0, "burn: zero lp");
require(totalSupply > 0, "burn: empty supply");
require(state.lmsr.nAssets > 0, "burn: uninit pool");
require(payerBalance >= lpAmount, "burn: insufficient LP");
// Refresh cached balances
for (uint i = 0; i < n; ) {
uint256 bal = IERC20(state.tokens[i]).balanceOf(address(this));
state.cachedUintBalances[i] = bal;
unchecked { i++; }
}
// Compute proportional withdrawal amounts
withdrawAmounts = burnReceiveAmounts(state, lpAmount, totalSupply);
// Transfer underlying tokens out
for (uint i = 0; i < n; ) {
if (withdrawAmounts[i] > 0) {
state.tokens[i].safeTransfer(receiver, withdrawAmounts[i]);
}
unchecked { i++; }
}
// Update cached balances and internal q
int128[] memory newQInternal = new int128[](n);
for (uint i = 0; i < n; ) {
uint256 bal = IERC20(state.tokens[i]).balanceOf(address(this));
state.cachedUintBalances[i] = bal;
newQInternal[i] = _uintToInternalFloor(bal, state.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) {
state.lmsr.deinit();
} else {
state.lmsr.updateForProportionalChange(newQInternal);
}
emit Burn(payer, receiver, withdrawAmounts, lpAmount);
}
/// @notice Get swap amounts for exact input swap
function swapAmounts(
State storage state,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice,
uint256 swapFeePpm,
bool stablePair
) internal view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(
state, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm, stablePair
);
return (grossIn, outUint, feeUint);
}
/// @notice Get swap amounts for swap to price limit
function swapToLimitAmounts(
State storage state,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
int128 limitPrice,
uint256 swapFeePpm
) internal view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapToLimit(
state, inputTokenIndex, outputTokenIndex, limitPrice, swapFeePpm
);
return (grossIn, outUint, feeUint);
}
/// @notice Execute exact input swap
function swap(
State storage state,
address payer,
address receiver,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice,
uint256 deadline,
uint256 swapFeePpm,
bool stablePair
) internal returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
uint256 n = state.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
uint256 prevBalI = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
uint256 prevBalJ = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this));
// Compute amounts
(uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalUsed, int128 amountOutInternal, , uint256 feeUint) =
_quoteSwapExactIn(state, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm, stablePair);
// Transfer exact amount from payer
state.tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount);
uint256 balIAfter = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
require(balIAfter == prevBalI + totalTransferAmount, "swap: non-standard tokenIn");
// Transfer output to receiver
state.tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint);
uint256 balJAfter = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this));
require(balJAfter == prevBalJ - amountOutUint, "swap: non-standard tokenOut");
// Update cached balances
state.cachedUintBalances[inputTokenIndex] = balIAfter;
state.cachedUintBalances[outputTokenIndex] = balJAfter;
// Apply swap to LMSR state
state.lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalUsed, amountOutInternal);
emit Swap(payer, receiver, state.tokens[inputTokenIndex], state.tokens[outputTokenIndex], totalTransferAmount, amountOutUint);
return (totalTransferAmount, amountOutUint, feeUint);
}
/// @notice Execute swap to price limit
function swapToLimit(
State storage state,
address payer,
address receiver,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
int128 limitPrice,
uint256 deadline,
uint256 swapFeePpm
) internal returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) {
uint256 n = state.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
uint256 prevBalI = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
uint256 prevBalJ = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this));
// Compute amounts
(uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalMax, int128 amountOutInternal, uint256 amountInUsedUint, uint256 feeUint) =
_quoteSwapToLimit(state, inputTokenIndex, outputTokenIndex, limitPrice, swapFeePpm);
// Transfer exact amount from payer
state.tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount);
uint256 balIAfter = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
require(balIAfter == prevBalI + totalTransferAmount, "swapToLimit: non-standard tokenIn");
// Transfer output to receiver
state.tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint);
uint256 balJAfter = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this));
require(balJAfter == prevBalJ - amountOutUint, "swapToLimit: non-standard tokenOut");
// Update caches
state.cachedUintBalances[inputTokenIndex] = balIAfter;
state.cachedUintBalances[outputTokenIndex] = balJAfter;
// Apply swap to LMSR state
state.lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalMax, amountOutInternal);
emit Swap(payer, receiver, state.tokens[inputTokenIndex], state.tokens[outputTokenIndex], amountInUsedUint, amountOutUint);
return (amountInUsedUint, amountOutUint, feeUint);
}
/// @notice Single-token mint (swapMint)
function swapMint(
State storage state,
address payer,
address receiver,
uint256 inputTokenIndex,
uint256 maxAmountIn,
uint256 deadline,
uint256 swapFeePpm,
uint256 totalSupply
) internal returns (uint256 lpMinted) {
uint256 n = state.tokens.length;
require(inputTokenIndex < n, "swapMint: idx");
require(maxAmountIn > 0, "swapMint: input zero");
require(deadline == 0 || block.timestamp <= deadline, "swapMint: deadline");
require(state.lmsr.nAssets > 0, "swapMint: uninit pool");
// Compute fee on gross maxAmountIn to get initial net estimate
(, uint256 netUintGuess) = _computeFee(maxAmountIn, swapFeePpm);
// Convert the net guess to internal (floor)
int128 netInternalGuess = _uintToInternalFloor(netUintGuess, state.bases[inputTokenIndex]);
require(netInternalGuess > int128(0), "swapMint: input too small after fee");
// Use LMSR view to determine actual internal consumed and size-increase
(int128 amountInInternalUsed, int128 sizeIncreaseInternal) = state.lmsr.swapAmountsForMint(inputTokenIndex, netInternalGuess);
// Convert to uint (ceil) to determine actual transfer
uint256 amountInUint = _internalToUintCeil(amountInInternalUsed, state.bases[inputTokenIndex]);
require(amountInUint > 0, "swapMint: input zero after internal conversion");
// Compute fee on actual used input and total transfer amount
uint256 feeUintActual = _ceilFee(amountInUint, swapFeePpm);
uint256 totalTransfer = amountInUint + feeUintActual;
require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMint: transfer exceeds max");
// Record pre-balance and transfer tokens
uint256 prevBalI = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
state.tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransfer);
uint256 balIAfter = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
require(balIAfter == prevBalI + totalTransfer, "swapMint: non-standard tokenIn");
// Update cached uint balances
state.cachedUintBalances[inputTokenIndex] = balIAfter;
// Compute old and new scaled size metrics
int128 oldTotal = _computeSizeMetric(state.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) {
actualLpToMint = newScaled;
} else {
require(oldScaled > 0, "swapMint: oldScaled zero");
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
if (delta > 0) {
actualLpToMint = (totalSupply * delta) / oldScaled;
} else {
actualLpToMint = 0;
}
}
require(actualLpToMint > 0, "swapMint: zero LP minted");
// Update LMSR internal state
int128[] memory newQInternal = new int128[](n);
for (uint256 idx = 0; idx < n; idx++) {
newQInternal[idx] = state.lmsr.qInternal[idx].mul(newTotal).div(oldTotal);
}
state.lmsr.updateForProportionalChange(newQInternal);
emit SwapMint(payer, receiver, inputTokenIndex, totalTransfer, amountInUint, feeUintActual);
emit Mint(payer, receiver, new uint256[](n), actualLpToMint);
return actualLpToMint;
}
/// @notice Burn LP tokens and swap to single asset (burnSwap)
function burnSwap(
State storage state,
address payer,
address receiver,
uint256 lpAmount,
uint256 inputTokenIndex,
uint256 deadline,
uint256 swapFeePpm,
uint256 totalSupply,
uint256 payerBalance
) internal returns (uint256 amountOutUint) {
uint256 n = state.tokens.length;
require(inputTokenIndex < n, "burnSwap: idx");
require(lpAmount > 0, "burnSwap: zero lp");
require(deadline == 0 || block.timestamp <= deadline, "burnSwap: deadline");
require(totalSupply > 0, "burnSwap: empty supply");
require(payerBalance >= lpAmount, "burnSwap: insufficient LP");
// alpha = lpAmount / supply as Q64.64
int128 alpha = ABDKMath64x64.divu(lpAmount, totalSupply);
// Use LMSR view to compute single-asset payout
(int128 payoutInternal, ) = state.lmsr.swapAmountsForBurn(inputTokenIndex, alpha);
// Convert payoutInternal -> uint (floor) to favor pool
amountOutUint = _internalToUintFloor(payoutInternal, state.bases[inputTokenIndex]);
require(amountOutUint > 0, "burnSwap: output zero");
// Apply swap fee to the output
if (swapFeePpm > 0) {
uint256 feeUint = _ceilFee(amountOutUint, swapFeePpm);
require(amountOutUint > feeUint, "burnSwap: fee exceeds output");
amountOutUint -= feeUint;
}
// Transfer the payout to receiver
state.tokens[inputTokenIndex].safeTransfer(receiver, amountOutUint);
// 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(state.tokens[idx]).balanceOf(address(this));
state.cachedUintBalances[idx] = bal;
newQInternal[idx] = _uintToInternalFloor(bal, state.bases[idx]);
}
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) {
state.lmsr.deinit();
} else {
state.lmsr.updateForProportionalChange(newQInternal);
}
emit Burn(payer, receiver, new uint256[](n), lpAmount);
return amountOutUint;
}
/// @notice Calculate flash loan repayment amounts
function flashRepaymentAmounts(
State storage state,
uint256[] memory loanAmounts,
uint256 flashFeePpm
) internal view returns (uint256[] memory repaymentAmounts) {
repaymentAmounts = new uint256[](state.tokens.length);
for (uint256 i = 0; i < state.tokens.length; i++) {
uint256 amount = loanAmounts[i];
if (amount > 0) {
repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm);
}
}
}
/// @notice Execute flash loan
function flash(
State storage state,
address recipient,
uint256[] memory amounts,
bytes calldata data,
uint256 flashFeePpm
) internal {
require(recipient != address(0), "flash: zero recipient");
require(amounts.length == state.tokens.length, "flash: amounts length mismatch");
// Calculate repayment amounts for each token including fee
uint256[] memory repaymentAmounts = new uint256[](state.tokens.length);
// Store initial balances to verify repayment later
uint256[] memory initialBalances = new uint256[](state.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 < state.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(state.tokens[i]).balanceOf(address(this));
// Transfer token to recipient
state.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 < state.tokens.length; i++) {
if (amounts[i] > 0) {
uint256 currentBalance = IERC20(state.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
state.cachedUintBalances[i] = currentBalance;
}
}
}
/// @notice Get marginal price between two tokens
function price(
State storage state,
uint256 baseTokenIndex,
uint256 quoteTokenIndex
) internal view returns (int128) {
uint256 n = state.tokens.length;
require(baseTokenIndex < n && quoteTokenIndex < n, "price: idx");
require(state.lmsr.nAssets > 0, "price: uninit");
return state.lmsr.price(baseTokenIndex, quoteTokenIndex);
}
/// @notice Get price of one LP token in quote asset
function poolPrice(
State storage state,
uint256 quoteTokenIndex,
uint256 totalSupply
) internal view returns (int128) {
uint256 n = state.tokens.length;
require(quoteTokenIndex < n, "poolPrice: idx");
require(state.lmsr.nAssets > 0, "poolPrice: uninit");
// price per unit of qTotal (Q64.64) from LMSR
int128 pricePerQ = state.lmsr.poolPrice(quoteTokenIndex);
// total internal q (qTotal) as Q64.64
int128 qTotal = _computeSizeMetric(state.lmsr.qInternal);
require(qTotal > int128(0), "poolPrice: qTotal zero");
// totalSupply as Q64.64
require(totalSupply > 0, "poolPrice: zero supply");
int128 supplyQ64 = ABDKMath64x64.fromUInt(totalSupply);
// factor = totalSupply / qTotal (Q64.64)
int128 factor = supplyQ64.div(qTotal);
// price per LP token = pricePerQ * factor (Q64.64)
return pricePerQ.mul(factor);
}
// Internal helper functions
/// @notice Ceiling fee helper
function _ceilFee(uint256 x, uint256 feePpm) internal pure returns (uint256) {
if (feePpm == 0) return 0;
return (x * feePpm + 1_000_000 - 1) / 1_000_000;
}
/// @notice Compute fee and net amounts for a gross input
function _computeFee(uint256 gross, uint256 swapFeePpm) internal pure returns (uint256 feeUint, uint256 netUint) {
if (swapFeePpm == 0) {
return (0, gross);
}
feeUint = _ceilFee(gross, swapFeePpm);
netUint = gross - feeUint;
}
/// @notice Add fee to net amount
function _addFee(uint256 netUint, uint256 swapFeePpm) internal pure returns (uint256 gross) {
if (swapFeePpm == 0) return netUint;
uint256 fee = _ceilFee(netUint, swapFeePpm);
return netUint + fee;
}
/// @notice Internal quote for exact-input swap
function _quoteSwapExactIn(
State storage state,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice,
uint256 swapFeePpm,
bool stablePair
)
internal
view
returns (
uint256 grossIn,
uint256 amountOutUint,
int128 amountInInternalUsed,
int128 amountOutInternal,
uint256 amountInUintNoFee,
uint256 feeUint
)
{
uint256 n = state.tokens.length;
require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx");
require(maxAmountIn > 0, "swap: input zero");
require(state.lmsr.nAssets > 0, "swap: empty pool");
// Estimate max net input
(, uint256 netUintForSwap) = _computeFee(maxAmountIn, swapFeePpm);
// Convert to internal (floor)
int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, state.bases[inputTokenIndex]);
require(deltaInternalI > int128(0), "swap: input too small after fee");
// Compute internal amounts using LMSR
(amountInInternalUsed, amountOutInternal) =
stablePair ? LMSRStabilizedBalancedPair.swapAmountsForExactInput(state.lmsr, inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice)
: state.lmsr.swapAmountsForExactInput(inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice);
// Convert actual used input internal -> uint (ceil)
amountInUintNoFee = _internalToUintCeil(amountInInternalUsed, state.bases[inputTokenIndex]);
require(amountInUintNoFee > 0, "swap: input zero");
// Compute gross transfer including fee
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, state.bases[outputTokenIndex]);
require(amountOutUint > 0, "swap: output zero");
}
/// @notice Internal quote for swap-to-limit
function _quoteSwapToLimit(
State storage state,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
int128 limitPrice,
uint256 swapFeePpm
)
internal
view
returns (
uint256 grossIn,
uint256 amountOutUint,
int128 amountInInternal,
int128 amountOutInternal,
uint256 amountInUintNoFee,
uint256 feeUint
)
{
uint256 n = state.tokens.length;
require(inputTokenIndex < n && outputTokenIndex < n, "swapToLimit: idx");
require(limitPrice > int128(0), "swapToLimit: limit <= 0");
require(state.lmsr.nAssets > 0, "swapToLimit: pool uninitialized");
// Compute internal maxima at the price limit
(amountInInternal, amountOutInternal) = state.lmsr.swapAmountsForPriceLimit(inputTokenIndex, outputTokenIndex, limitPrice);
// Convert input to uint (ceil) and output to uint (floor)
amountInUintNoFee = _internalToUintCeil(amountInInternal, state.bases[inputTokenIndex]);
require(amountInUintNoFee > 0, "swapToLimit: input zero");
feeUint = 0;
grossIn = amountInUintNoFee;
if (swapFeePpm > 0) {
feeUint = _ceilFee(amountInUintNoFee, swapFeePpm);
grossIn += feeUint;
}
amountOutUint = _internalToUintFloor(amountOutInternal, state.bases[outputTokenIndex]);
require(amountOutUint > 0, "swapToLimit: output zero");
}
// Convert uint token amount -> internal 64.64 (floor)
function _uintToInternalFloor(uint256 amount, uint256 base) internal pure returns (int128) {
return ABDKMath64x64.divu(amount, base);
}
// Convert internal 64.64 -> uint token amount (floor)
function _internalToUintFloor(int128 internalAmount, uint256 base) internal pure returns (uint256) {
return ABDKMath64x64.mulu(internalAmount, base);
}
// Convert internal 64.64 -> uint token amount (ceiling)
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
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;
}
/// @notice Helper to compute size metric (sum of all asset quantities) from internal balances
function _computeSizeMetric(int128[] memory qInternal_) internal pure returns (int128) {
int128 total = int128(0);
for (uint i = 0; i < qInternal_.length; ) {
total = total.add(qInternal_[i]);
unchecked { i++; }
}
return total;
}
}