Compare commits

10 Commits

Author SHA1 Message Date
tim
c002d26daf protocol fees 2025-10-02 16:43:02 -04:00
tim
b7e1b1cac2 LMSRStabilized pure refactor; swapMintAmounts 2025-10-01 17:09:42 -04:00
tim
5a2e7039d1 LMSRStabilized pure refactor; swapMintAmounts 2025-10-01 17:08:02 -04:00
tim
a6f6fd034c bugfix 2025-10-01 15:22:04 -04:00
tim
d46e60f83c bugfix 2025-10-01 15:20:02 -04:00
tim
3a5f0842b3 burnAmounts 2025-10-01 15:17:48 -04:00
tim
40e1d25e72 mintAmounts 2025-10-01 15:02:11 -04:00
tim
5ce14ab2e1 linted 2025-09-30 15:42:52 -04:00
tim
91e6a916ac burnSwap fees 2025-09-29 16:10:30 -04:00
tim
28b9474363 PartyPoolMintImpl 2025-09-26 11:48:01 -04:00
14 changed files with 957 additions and 378 deletions

View File

@@ -9,10 +9,10 @@ remappings = [
optimizer=true
optimizer_runs=999999999
viaIR=true
gas_reports = ['PartyPool', 'PartyPlanner', 'PartyPoolSwapMintImpl', 'PartyPoolViewImpl']
gas_reports = ['PartyPool', 'PartyPlanner', 'PartyPoolSwapMintImpl', 'PartyPoolMintImpl',]
fs_permissions = [{ access = "write", path = "chain.json"}]
[lint]
exclude_lints=['mixed-case-variable', 'unaliased-plain-import', ]
lint_on_build=false # more annoying than helpful
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

View File

@@ -12,9 +12,9 @@ import "forge-std/console2.sol";
contract DeployMock is Script {
address constant devAccount0 = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
address constant public DEV_ACCOUNT_0 = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
// private key 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356
address constant devAccount7 = 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955;
address constant public DEV_ACCOUNT_7 = 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955;
function run() public {
vm.startBroadcast();
@@ -69,14 +69,14 @@ contract DeployMock is Script {
_feePpm,
false,
msg.sender, // payer: this script
devAccount7, // receiver of initial LP
DEV_ACCOUNT_7, // receiver of initial LP
initialDeposits,
initialLpAmount,
deadline
);
// give tokens to dev7 for later use
mintAll(devAccount7, 1_000_000);
mintAll(DEV_ACCOUNT_7, 1_000_000);
vm.stopBroadcast();

View File

@@ -4,13 +4,17 @@ pragma solidity ^0.8.30;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {PartyPool} from "./PartyPool.sol";
import {PartyPoolSwapMintImpl} from "./PartyPoolSwapMintImpl.sol";
import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol";
import {PartyPlanner} from "./PartyPlanner.sol";
library Deploy {
function newPartyPlanner() internal returns (PartyPlanner) {
return new PartyPlanner(
new PartyPoolSwapMintImpl()
new PartyPoolSwapMintImpl(),
new PartyPoolMintImpl(),
0, // protocolFeePpm = 0 for deploy helper
address(0) // protocolFeeAddress = address(0) for deploy helper
);
}
@@ -24,8 +28,23 @@ library Deploy {
uint256 _flashFeePpm,
bool _stable
) internal returns (PartyPool) {
return new PartyPool(name_, symbol_, tokens_, bases_, _kappa, _swapFeePpm, _flashFeePpm, _stable,
new PartyPoolSwapMintImpl()
// default protocol fee/off parameters (per your instruction) - set to 0 / address(0)
uint256 protocolFeePpm = 0;
address protocolAddr = address(0);
return new PartyPool(
name_,
symbol_,
tokens_,
bases_,
_kappa,
_swapFeePpm,
_flashFeePpm,
protocolFeePpm,
protocolAddr,
_stable,
new PartyPoolSwapMintImpl(),
new PartyPoolMintImpl()
);
}

View File

@@ -117,6 +117,9 @@ interface IPartyPlanner {
function getPoolsByToken(IERC20 token, uint256 offset, uint256 limit) external view returns (PartyPool[] memory pools);
/// @notice Address of the SwapMint implementation contract used by all pools created by this factory
function swapMintImpl() external view returns (address);
function mintImpl() external view returns (PartyPoolMintImpl);
/// @notice Address of the SwapMint implementation contract used by all pools created by this factory
function swapMintImpl() external view returns (PartyPoolSwapMintImpl);
}

View File

@@ -75,19 +75,36 @@ interface IPartyPool is IERC20Metadata {
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
function flashFeePpm() external view returns (uint256);
/// @notice Protocol fee share (ppm) applied to fees collected by the pool (floored when accrued)
/// @dev This is the fraction (in ppm) of the pool-collected fees that are owed to the protocol.
function protocolFeePpm() external view returns (uint256);
/// @notice Address that will receive collected protocol tokens when collectProtocolFees() is called.
function protocolFeeAddress() external view returns (address);
/// @notice Per-token protocol fee ledger accessor. Returns tokens owed (raw uint token units) for token index i.
function protocolFeesOwed(uint256) external view returns (uint256);
/// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q)
/// @dev Pools are constructed with a κ value; this getter exposes the κ used by the pool.
function kappa() external view returns (int128);
// Initialization / Mint / Burn (LP token managed)
/// @notice Initial mint to set up pool for the first time.
/// @dev Assumes tokens have already been transferred to the pool prior to calling.
/// Can only be called when the pool is uninitialized (totalSupply() == 0 or lmsr.nAssets == 0).
/// @param receiver address that receives the LP tokens
/// @param lpTokens The number of LP tokens to issue for this mint. If 0, then the number of tokens returned will equal the LMSR internal q total
function initialMint(address receiver, uint256 lpTokens) external returns (uint256 lpMinted);
/// @notice Calculate the proportional deposit amounts required for a given LP token amount
/// @dev Returns the minimum token amounts (rounded up) that must be supplied to receive lpTokenAmount
/// LP tokens at current pool proportions. If the pool is empty (initial deposit) returns zeros
/// because the initial deposit is handled by transferring tokens then calling mint().
/// @param lpTokenAmount The amount of LP tokens desired
/// @return depositAmounts Array of token amounts to deposit (rounded up)
function mintDepositAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory depositAmounts);
function mintAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory depositAmounts);
/// @notice Proportional mint (or initial supply if first call).
/// @dev - For initial supply: assumes tokens have already been transferred to the pool prior to calling.
@@ -105,7 +122,7 @@ interface IPartyPool is IERC20Metadata {
/// If the pool is uninitialized or supply is zero, returns zeros.
/// @param lpTokenAmount The amount of LP tokens to burn
/// @return withdrawAmounts Array of token amounts to withdraw (rounded down)
function burnReceiveAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts);
function burnAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts);
/// @notice Burn LP tokens and withdraw the proportional basket to receiver.
/// @dev Payer must own or approve the LP tokens being burned. The function updates LMSR state
@@ -114,7 +131,7 @@ interface IPartyPool is IERC20Metadata {
/// @param receiver address that receives the withdrawn tokens
/// @param lpAmount amount of LP tokens to burn (proportional withdrawal)
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external;
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external returns (uint256[] memory withdrawAmounts);
// Swaps

View File

@@ -64,8 +64,15 @@ library LMSRStabilized {
/// @notice Cost C(q) = b * (M + ln(Z))
function cost(State storage s) internal view returns (int128) {
int128 b = _computeB(s);
(int128 M, int128 Z) = _computeMAndZ(b, s.qInternal);
return cost(s.kappa, s.qInternal);
}
/// @notice Pure version: Cost C(q) = b * (M + ln(Z))
function cost(int128 kappa, int128[] memory qInternal) internal pure returns (int128) {
int128 sizeMetric = _computeSizeMetric(qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
(int128 M, int128 Z) = _computeMAndZ(b, qInternal);
int128 lnZ = _ln(Z);
int128 inner = M.add(lnZ);
int128 c = b.mul(inner);
@@ -102,23 +109,58 @@ library LMSRStabilized {
int128 a,
int128 limitPrice
) internal view returns (int128 amountIn, int128 amountOut) {
require(i < s.nAssets && j < s.nAssets, "LMSR: idx");
return swapAmountsForExactInput(s.nAssets, s.kappa, s.qInternal, i, j, a, limitPrice);
}
/// @notice Pure version: Closed-form asset-i -> asset-j amountOut in 64.64 fixed-point format (fee-free kernel)
/// Uses the closed-form two-asset LMSR formula (no fees in kernel):
/// y = b * ln(1 + r0 * (1 - exp(-a / b)))
/// where r0 = e_i / e_j.
///
/// This variant accepts an additional `limitPrice` (64.64) which represents the
/// maximum acceptable marginal price (p_i / p_j). If the marginal price would
/// exceed `limitPrice` before the requested `a` is fully consumed, the input
/// `a` is truncated to the value that makes the marginal price equal `limitPrice`.
///
/// NOTE: Kernel is fee-free; fees should be handled by the wrapper/token layer.
///
/// @param nAssets Number of assets in the pool
/// @param kappa Liquidity parameter κ (64.64 fixed point)
/// @param qInternal Cached internal balances in 64.64 fixed-point format
/// @param i Index of input asset
/// @param j Index of output asset
/// @param a Amount of input asset (in int128 format, 64.64 fixed-point)
/// @param limitPrice Maximum acceptable price ratio (64.64). If <= current price, this call reverts.
/// @return amountIn Actual amount of input asset used (may be less than `a` if limited by price)
/// @return amountOut Amount of output asset j in 64.64 fixed-point format
function swapAmountsForExactInput(
uint256 nAssets,
int128 kappa,
int128[] memory qInternal,
uint256 i,
uint256 j,
int128 a,
int128 limitPrice
) internal pure returns (int128 amountIn, int128 amountOut) {
require(i < nAssets && j < nAssets, "LMSR: idx");
// Initialize amountIn to full amount (will be adjusted if limit price is hit)
amountIn = a;
// Compute b and ensure positivity before deriving invB
int128 b = _computeB(s);
int128 sizeMetric = _computeSizeMetric(qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
require(b > int128(0), "LMSR: b<=0");
// Precompute reciprocal of b to avoid repeated divisions
int128 invB = ABDKMath64x64.div(ONE, b);
// Guard: output asset must have non-zero effective weight to avoid degenerate/div-by-zero-like conditions
require(s.qInternal[j] > int128(0), "LMSR: e_j==0");
require(qInternal[j] > int128(0), "LMSR: e_j==0");
// Compute r0 = exp((q_i - q_j) / b) directly using invB
int128 r0 = _exp(s.qInternal[i].sub(s.qInternal[j]).mul(invB));
int128 r0 = _exp(qInternal[i].sub(qInternal[j]).mul(invB));
require(r0 > int128(0), "LMSR: r0<=0"); // equivalent to e_j > 0 check
// If a positive limitPrice is given, determine whether the full `a` would
@@ -203,7 +245,7 @@ library LMSRStabilized {
// If inner <= 0 then cap output to the current balance q_j (cannot withdraw more than q_j)
if (inner <= int128(0)) {
console2.log("WARNING: inner <= 0, capping output to balance q_j");
int128 qj64 = s.qInternal[j];
int128 qj64 = qInternal[j];
console2.log("Capped output (64.64):");
console2.logInt(qj64);
return (amountIn, qj64);
@@ -249,21 +291,48 @@ library LMSRStabilized {
uint256 j,
int128 limitPrice
) internal view returns (int128 amountIn, int128 amountOut) {
require(i < s.nAssets && j < s.nAssets, "LMSR: idx");
return swapAmountsForPriceLimit(s.nAssets, s.kappa, s.qInternal, i, j, limitPrice);
}
/// @notice Pure version: Maximum input/output pair possible when swapping from asset i to asset j
/// given a maximum acceptable price ratio (p_i/p_j).
/// Returns the input amount that would drive the marginal price to the limit (amountIn)
/// and the corresponding output amount (amountOut). If the output would exceed the
/// j-balance, amountOut is capped and amountIn is solved for the capped output.
///
/// @param nAssets Number of assets in the pool
/// @param kappa Liquidity parameter κ (64.64 fixed point)
/// @param qInternal Cached internal balances in 64.64 fixed-point format
/// @param i Index of input asset
/// @param j Index of output asset
/// @param limitPrice Maximum acceptable price ratio (64.64)
/// @return amountIn Maximum input amount in 64.64 fixed-point format that reaches the price limit
/// @return amountOut Corresponding maximum output amount in 64.64 fixed-point format
function swapAmountsForPriceLimit(
uint256 nAssets,
int128 kappa,
int128[] memory qInternal,
uint256 i,
uint256 j,
int128 limitPrice
) internal pure returns (int128 amountIn, int128 amountOut) {
require(i < nAssets && j < nAssets, "LMSR: idx");
require(limitPrice > int128(0), "LMSR: limitPrice <= 0");
// Compute b and ensure positivity before deriving invB
int128 b = _computeB(s);
int128 sizeMetric = _computeSizeMetric(qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
require(b > int128(0), "LMSR: b<=0");
// Precompute reciprocal of b to avoid repeated divisions
int128 invB = ABDKMath64x64.div(ONE, b);
// Guard: output asset must have non-zero effective weight to avoid degenerate/div-by-zero-like conditions
require(s.qInternal[j] > int128(0), "LMSR: e_j==0");
require(qInternal[j] > int128(0), "LMSR: e_j==0");
// Compute r0 = exp((q_i - q_j) / b) directly using invB
int128 r0 = _exp(s.qInternal[i].sub(s.qInternal[j]).mul(invB));
int128 r0 = _exp(qInternal[i].sub(qInternal[j]).mul(invB));
console2.log("\n=== Max Input/Output Calculation ===");
console2.log("Limit price (64x64):");
@@ -316,7 +385,7 @@ library LMSRStabilized {
console2.logInt(maxOutput);
// Current balance of asset j (in 64.64)
int128 qj64 = s.qInternal[j];
int128 qj64 = qInternal[j];
console2.log("Current j balance (64.64):");
console2.logInt(qj64);
@@ -366,20 +435,45 @@ library LMSRStabilized {
uint256 i,
int128 a
) internal view returns (int128 amountIn, int128 amountOut) {
require(i < s.nAssets, "LMSR: idx");
return swapAmountsForMint(s.nAssets, s.kappa, s.qInternal, i, a);
}
/// @notice Pure version: Compute LP-size increase when minting from a single-token input using bisection only.
/// @dev Solve for α >= 0 such that:
/// a = α*q_i + sum_{j != i} x_j(α)
/// where x_j(α) is the input to swap i->j that yields y_j = α*q_j and
/// x_j = b * ln( r0_j / (r0_j + 1 - exp(y_j / b)) ), r0_j = exp((q_i - q_j)/b).
/// Bisection is used (no Newton) to keep implementation compact and gas-friendly.
/// @param nAssets Number of assets in the pool
/// @param kappa Liquidity parameter κ (64.64 fixed point)
/// @param qInternal Cached internal balances in 64.64 fixed-point format
/// @param i Index of input asset
/// @param a Amount of input asset (in int128 format, 64.64 fixed-point)
/// @return amountIn Actual amount of input consumed
/// @return amountOut LP size-metric increase (alpha * S)
function swapAmountsForMint(
uint256 nAssets,
int128 kappa,
int128[] memory qInternal,
uint256 i,
int128 a
) internal pure returns (int128 amountIn, int128 amountOut) {
require(i < nAssets, "LMSR: idx");
require(a > int128(0), "LMSR: amount <= 0");
int128 b = _computeB(s);
int128 sizeMetric = _computeSizeMetric(qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
require(b > int128(0), "LMSR: b<=0");
int128 invB = ABDKMath64x64.div(ONE, b);
int128 S = _computeSizeMetric(s.qInternal);
int128 S = sizeMetric;
uint256 n = s.nAssets;
uint256 n = nAssets;
// Precompute r0_j = exp((q_i - q_j) / b) for all j to avoid recomputing during search.
int128[] memory r0 = new int128[](n);
for (uint256 j = 0; j < n; ) {
r0[j] = _exp(s.qInternal[i].sub(s.qInternal[j]).mul(invB));
r0[j] = _exp(qInternal[i].sub(qInternal[j]).mul(invB));
unchecked { j++; }
}
@@ -415,7 +509,7 @@ library LMSRStabilized {
// loop j != i
for (uint256 j = 0; j < n; ) {
if (j != i) {
int128 yj = alpha.mul(s.qInternal[j]); // target output y_j = alpha * q_j
int128 yj = alpha.mul(qInternal[j]); // target output y_j = alpha * q_j
if (yj > int128(0)) {
int128 expArg = yj.mul(invB);
// Guard exp arg
@@ -433,7 +527,7 @@ library LMSRStabilized {
unchecked { j++; }
}
int128 aReq = fail ? int128(type(int128).max) : alpha.mul(s.qInternal[i]).add(sumX);
int128 aReq = fail ? int128(type(int128).max) : alpha.mul(qInternal[i]).add(sumX);
if (aReq >= a || high >= alphaCap) {
break;
@@ -455,7 +549,7 @@ library LMSRStabilized {
for (uint256 j = 0; j < n; ) {
if (j != i) {
int128 yj = alpha.mul(s.qInternal[j]);
int128 yj = alpha.mul(qInternal[j]);
if (yj > int128(0)) {
int128 expArg = yj.mul(invB);
if (expArg > EXP_LIMIT) { fail = true; break; }
@@ -472,7 +566,7 @@ library LMSRStabilized {
unchecked { j++; }
}
int128 aReq = fail ? int128(type(int128).max) : alpha.mul(s.qInternal[i]).add(sumX);
int128 aReq = fail ? int128(type(int128).max) : alpha.mul(qInternal[i]).add(sumX);
if (aReq > a) {
// mid requires more input than provided -> decrease alpha
@@ -502,7 +596,7 @@ library LMSRStabilized {
bool failFinal = false;
for (uint256 j = 0; j < n; ) {
if (j != i) {
int128 yj = alphaFinal.mul(s.qInternal[j]);
int128 yj = alphaFinal.mul(qInternal[j]);
if (yj > int128(0)) {
int128 expArg = yj.mul(invB);
if (expArg > EXP_LIMIT) { failFinal = true; break; }
@@ -524,7 +618,7 @@ library LMSRStabilized {
return (int128(0), int128(0));
}
int128 aRequired = alphaFinal.mul(s.qInternal[i]).add(sumXFinal);
int128 aRequired = alphaFinal.mul(qInternal[i]).add(sumXFinal);
// amountIn is actual consumed input (may be <= provided a)
amountIn = aRequired;
@@ -553,28 +647,56 @@ library LMSRStabilized {
uint256 i,
int128 alpha
) internal view returns (int128 amountOut, int128 amountIn) {
require(i < s.nAssets, "LMSR: idx");
return swapAmountsForBurn(s.nAssets, s.kappa, s.qInternal, i, alpha);
}
/// @notice Pure version: Compute single-asset payout when burning a proportional share alpha of the pool.
/// @dev Simulate q_after = (1 - alpha) * q, return the amount of asset `i` the burner
/// would receive after swapping each other asset's withdrawn portion into `i`.
/// For each j != i:
/// - wrapper holds a_j = alpha * q_j
/// - swap j->i with closed-form exact-input formula using the current q_local
/// - cap output to q_local[i] when necessary (solve inverse for input used)
/// Treat any per-asset rhs<=0 as "this asset contributes zero" (do not revert).
/// Revert only if the final single-asset payout is zero.
/// @param nAssets Number of assets in the pool
/// @param kappa Liquidity parameter κ (64.64 fixed point)
/// @param qInternal Cached internal balances in 64.64 fixed-point format
/// @param i Index of output asset
/// @param alpha Proportional share to burn (0 < alpha <= 1)
/// @return amountOut Amount of asset i received (in 64.64 fixed-point)
/// @return amountIn LP size-metric redeemed (alpha * S)
function swapAmountsForBurn(
uint256 nAssets,
int128 kappa,
int128[] memory qInternal,
uint256 i,
int128 alpha
) internal pure returns (int128 amountOut, int128 amountIn) {
require(i < nAssets, "LMSR: idx");
require(alpha > int128(0) && alpha <= ONE, "LMSR: alpha");
int128 b = _computeB(s);
int128 sizeMetric = _computeSizeMetric(qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
require(b > int128(0), "LMSR: b<=0");
int128 invB = ABDKMath64x64.div(ONE, b);
uint256 n = s.nAssets;
uint256 n = nAssets;
// Size metric and burned size (amountIn returned)
int128 S = _computeSizeMetric(s.qInternal);
int128 S = sizeMetric;
amountIn = alpha.mul(S); // total size-metric redeemed
// Build q_local := q_after_burn = (1 - alpha) * q
int128[] memory qLocal = new int128[](n);
for (uint256 j = 0; j < n; ) {
qLocal[j] = s.qInternal[j].mul(ONE.sub(alpha));
qLocal[j] = qInternal[j].mul(ONE.sub(alpha));
unchecked { j++; }
}
// Start totalOut with direct portion of asset i redeemed
int128 totalOut = alpha.mul(s.qInternal[i]);
int128 totalOut = alpha.mul(qInternal[i]);
// Track whether any non-zero contribution was produced
bool anyNonZero = (totalOut > int128(0));
@@ -582,7 +704,7 @@ library LMSRStabilized {
// For each asset j != i, swap the withdrawn a_j := alpha * q_j into i
for (uint256 j = 0; j < n; ) {
if (j != i) {
int128 aj = alpha.mul(s.qInternal[j]); // wrapper-held withdrawn amount of j
int128 aj = alpha.mul(qInternal[j]); // wrapper-held withdrawn amount of j
if (aj > int128(0)) {
// expArg = aj / b
int128 expArg = aj.mul(invB);
@@ -722,15 +844,26 @@ library LMSRStabilized {
/// @notice Price-share of asset i: exp(z_i) / Z (64.64)
function priceShare(State storage s, uint256 i) internal view returns (int128) {
int128 b = _computeB(s);
uint len = s.qInternal.length;
return priceShare(s.kappa, s.qInternal, i);
}
/// @notice Pure version: Price-share of asset i: exp(z_i) / Z (64.64)
/// @param kappa Liquidity parameter κ (64.64 fixed point)
/// @param qInternal Cached internal balances in 64.64 fixed-point format
/// @param i Index of asset
/// @return Price share in 64.64 fixed-point format
function priceShare(int128 kappa, int128[] memory qInternal, uint256 i) internal pure returns (int128) {
int128 sizeMetric = _computeSizeMetric(qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
uint len = qInternal.length;
require(len > 0, "LMSR: no assets");
// Precompute reciprocal of b and perform a single pass that tracks M, Z, and e_i
int128 invB = ABDKMath64x64.div(ONE, b);
// Initialize from the first element
int128 M = s.qInternal[0].mul(invB);
int128 M = qInternal[0].mul(invB);
int128 Z = ONE; // exp(0)
int128 e_i_acc;
bool setEi;
@@ -741,7 +874,7 @@ library LMSRStabilized {
}
for (uint idx = 1; idx < len; ) {
int128 yi = s.qInternal[idx].mul(invB);
int128 yi = qInternal[idx].mul(invB);
if (yi <= M) {
// Add contribution under current center
int128 term = _exp(yi.sub(M));
@@ -770,7 +903,7 @@ library LMSRStabilized {
if (!setEi) {
// Only possible when len == 1 and i != 0, guarded by caller invariants typically
// Fallback: compute directly (kept for completeness)
int128 yi = s.qInternal[i].mul(invB);
int128 yi = qInternal[i].mul(invB);
e_i_acc = _exp(yi.sub(M));
}
@@ -780,27 +913,54 @@ library LMSRStabilized {
/// @notice Marginal price of `base` in terms of `quote` (p_quote / p_base) as Q64.64
/// @dev Returns exp((q_quote - q_base) / b). Indices must be valid and b > 0.
function price(State storage s, uint256 baseTokenIndex, uint256 quoteTokenIndex) internal view returns (int128) {
require(baseTokenIndex < s.nAssets && quoteTokenIndex < s.nAssets, "LMSR: idx");
int128 b = _computeB(s);
return price(s.nAssets, s.kappa, s.qInternal, baseTokenIndex, quoteTokenIndex);
}
/// @notice Pure version: Marginal price of `base` in terms of `quote` (p_quote / p_base) as Q64.64
/// @dev Returns exp((q_quote - q_base) / b). Indices must be valid and b > 0.
/// @param nAssets Number of assets in the pool
/// @param kappa Liquidity parameter κ (64.64 fixed point)
/// @param qInternal Cached internal balances in 64.64 fixed-point format
/// @param baseTokenIndex Index of base token
/// @param quoteTokenIndex Index of quote token
/// @return Price in 64.64 fixed-point format
function price(uint256 nAssets, int128 kappa, int128[] memory qInternal, uint256 baseTokenIndex, uint256 quoteTokenIndex) internal pure returns (int128) {
require(baseTokenIndex < nAssets && quoteTokenIndex < nAssets, "LMSR: idx");
int128 sizeMetric = _computeSizeMetric(qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
require(b > int128(0), "LMSR: b<=0");
// Use reciprocal of b to avoid repeated divisions
int128 invB = ABDKMath64x64.div(ONE, b);
// Marginal price p_quote / p_base = exp((q_quote - q_base) / b)
return _exp(s.qInternal[quoteTokenIndex].sub(s.qInternal[baseTokenIndex]).mul(invB));
return _exp(qInternal[quoteTokenIndex].sub(qInternal[baseTokenIndex]).mul(invB));
}
/// @notice Price of one unit of the LP size-metric (S = sum q_i) denominated in `quote` asset (Q64.64)
/// @dev Computes: poolPrice_quote = (1 / S) * sum_j q_j * exp((q_j - q_quote) / b)
function poolPrice(State storage s, uint256 quoteTokenIndex) internal view returns (int128) {
require(quoteTokenIndex < s.nAssets, "LMSR: idx");
return poolPrice(s.nAssets, s.kappa, s.qInternal, quoteTokenIndex);
}
/// @notice Pure version: Price of one unit of the LP size-metric (S = sum q_i) denominated in `quote` asset (Q64.64)
/// @dev Computes: poolPrice_quote = (1 / S) * sum_j q_j * exp((q_j - q_quote) / b)
/// @param nAssets Number of assets in the pool
/// @param kappa Liquidity parameter κ (64.64 fixed point)
/// @param qInternal Cached internal balances in 64.64 fixed-point format
/// @param quoteTokenIndex Index of quote token
/// @return Pool price in 64.64 fixed-point format
function poolPrice(uint256 nAssets, int128 kappa, int128[] memory qInternal, uint256 quoteTokenIndex) internal pure returns (int128) {
require(quoteTokenIndex < nAssets, "LMSR: idx");
// Compute b and ensure positivity
int128 b = _computeB(s);
int128 sizeMetric = _computeSizeMetric(qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
require(b > int128(0), "LMSR: b<=0");
// Compute total size metric S = sum q_i
int128 S = _computeSizeMetric(s.qInternal);
int128 S = sizeMetric;
require(S > int128(0), "LMSR: size zero");
// Precompute reciprocal of b
@@ -808,12 +968,12 @@ library LMSRStabilized {
// Accumulate weighted exponentials: sum_j q_j * exp((q_j - q_quote) / b)
int128 acc = int128(0);
uint256 n = s.nAssets;
uint256 n = nAssets;
for (uint256 j = 0; j < n; ) {
// factor = exp((q_j - q_quote) / b)
int128 factor = _exp(s.qInternal[j].sub(s.qInternal[quoteTokenIndex]).mul(invB));
int128 factor = _exp(qInternal[j].sub(qInternal[quoteTokenIndex]).mul(invB));
// term = q_j * factor
int128 term = s.qInternal[j].mul(factor);
int128 term = qInternal[j].mul(factor);
acc = acc.add(term);
unchecked { j++; }
}

View File

@@ -7,15 +7,29 @@ import "./LMSRStabilized.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {PartyPoolSwapMintImpl} from "./PartyPoolSwapMintImpl.sol";
import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol";
/// @title PartyPlanner
/// @notice Factory contract for creating and tracking PartyPool instances
contract PartyPlanner is IPartyPlanner {
using SafeERC20 for IERC20;
int128 private constant FIXED_ONE_64x64 = int128(1) << 64;
int128 private constant ONE = int128(1) << 64;
/// @notice Address of the Mint implementation contract used by all pools created by this factory
PartyPoolMintImpl private immutable MINT_IMPL;
function mintImpl() external view returns (PartyPoolMintImpl) { return MINT_IMPL; }
/// @notice Address of the SwapMint implementation contract used by all pools created by this factory
address public immutable swapMintImpl;
PartyPoolSwapMintImpl private immutable SWAP_MINT_IMPL;
function swapMintImpl() external view returns (PartyPoolSwapMintImpl) { return SWAP_MINT_IMPL; }
/// @notice Protocol fee share (ppm) applied to fees collected by pools created by this planner
uint256 private immutable PROTOCOL_FEE_PPM;
function protocolFeePpm() external view returns (uint256) { return PROTOCOL_FEE_PPM; }
/// @notice Address to receive protocol fees for pools created by this planner (may be address(0))
address private immutable PROTOCOL_FEE_ADDRESS;
function protocolFeeAddress() external view returns (address) { return PROTOCOL_FEE_ADDRESS; }
// On-chain pool indexing
PartyPool[] private _allPools;
@@ -25,9 +39,23 @@ contract PartyPlanner is IPartyPlanner {
mapping(IERC20 => PartyPool[]) private _poolsByToken;
/// @param _swapMintImpl address of the SwapMint implementation contract to be used by all pools
constructor(PartyPoolSwapMintImpl _swapMintImpl) {
require(address(_swapMintImpl) != address(0), "Planner: impl address cannot be zero");
swapMintImpl = address(_swapMintImpl);
/// @param _mintImpl address of the Mint implementation contract to be used by all pools
/// @param _protocolFeePpm protocol fee share (ppm) to be used for pools created by this planner
/// @param _protocolFeeAddress recipient address for protocol fees for pools created by this planner (may be address(0))
constructor(
PartyPoolSwapMintImpl _swapMintImpl,
PartyPoolMintImpl _mintImpl,
uint256 _protocolFeePpm,
address _protocolFeeAddress
) {
require(address(_swapMintImpl) != address(0), "Planner: swapMintImpl address cannot be zero");
SWAP_MINT_IMPL = _swapMintImpl;
require(address(_mintImpl) != address(0), "Planner: mintImpl address cannot be zero");
MINT_IMPL = _mintImpl;
require(_protocolFeePpm < 1_000_000, "Planner: protocol fee >= ppm");
PROTOCOL_FEE_PPM = _protocolFeePpm;
PROTOCOL_FEE_ADDRESS = _protocolFeeAddress;
}
/// Main newPool variant: accepts kappa directly (preferred).
@@ -66,8 +94,11 @@ contract PartyPlanner is IPartyPlanner {
_kappa,
_swapFeePpm,
_flashFeePpm,
PROTOCOL_FEE_PPM,
PROTOCOL_FEE_ADDRESS,
_stable,
PartyPoolSwapMintImpl(swapMintImpl)
PartyPoolSwapMintImpl(SWAP_MINT_IMPL),
MINT_IMPL
);
_allPools.push(pool);
@@ -122,8 +153,8 @@ contract PartyPlanner is IPartyPlanner {
uint256 deadline
) external returns (PartyPool pool, uint256 lpAmount) {
// Validate fixed-point fractions: must be less than 1.0 in 64.64 fixed-point
require(_tradeFrac < FIXED_ONE_64x64, "Planner: tradeFrac must be < 1 (64.64)");
require(_targetSlippage < FIXED_ONE_64x64, "Planner: targetSlippage must be < 1 (64.64)");
require(_tradeFrac < ONE, "Planner: tradeFrac must be < 1 (64.64)");
require(_targetSlippage < ONE, "Planner: targetSlippage must be < 1 (64.64)");
// Compute kappa from slippage params using LMSR helper (kappa depends only on n, f and s)
int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(_tokens.length, _tradeFrac, _targetSlippage);

View File

@@ -12,6 +12,7 @@ import "./IPartyPool.sol";
import "./IPartyFlashCallback.sol";
import "./PartyPoolBase.sol";
import {PartyPoolSwapMintImpl} from "./PartyPoolSwapMintImpl.sol";
import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol";
/// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token
/// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model.
@@ -34,19 +35,36 @@ contract PartyPool is PartyPoolBase, IPartyPool {
/// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q)
/// @dev Pool is constructed with a fixed κ. Clients that previously passed tradeFrac/targetSlippage
/// should use LMSRStabilized.computeKappaFromSlippage(...) to derive κ and pass it here.
int128 public immutable kappa; // kappa in Q64.64
int128 private immutable KAPPA; // kappa in Q64.64
function kappa() external view returns (int128) { return KAPPA; }
/// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations.
uint256 public immutable swapFeePpm;
uint256 private immutable SWAP_FEE_PPM;
function swapFeePpm() external view returns (uint256) { return SWAP_FEE_PPM; }
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
uint256 public immutable flashFeePpm;
uint256 private immutable FLASH_FEE_PPM;
function flashFeePpm() external view returns (uint256) { return FLASH_FEE_PPM; }
/// @notice Protocol fee share (ppm) applied to fees collected by the pool (floored when accrued)
uint256 private immutable PROTOCOL_FEE_PPM;
function protocolFeePpm() external view returns (uint256) { return PROTOCOL_FEE_PPM; }
/// @notice Address to which collected protocol tokens will be sent on collectProtocolFees()
address private immutable PROTOCOL_FEE_ADDRESS;
function protocolFeeAddress() external view returns (address) { return PROTOCOL_FEE_ADDRESS; }
/// @notice If true and there are exactly two assets, an optimized 2-asset stable-pair path is used for some computations.
bool immutable private _stablePair; // if true, the optimized LMSRStabilizedBalancedPair optimization path is enabled
bool immutable private IS_STABLE_PAIR; // if true, the optimized LMSRStabilizedBalancedPair optimization path is enabled
/// @notice Address of the Mint implementation contract for delegatecall
PartyPoolMintImpl private immutable MINT_IMPL;
function mintImpl() external view returns (PartyPoolMintImpl) { return MINT_IMPL; }
/// @notice Address of the SwapMint implementation contract for delegatecall
address public immutable swapMintImpl;
PartyPoolSwapMintImpl private immutable SWAP_MINT_IMPL;
function swapMintImpl() external view returns (PartyPoolSwapMintImpl) { return SWAP_MINT_IMPL; }
/// @inheritdoc IPartyPool
function getToken(uint256 i) external view returns (IERC20) { return tokens[i]; }
@@ -64,34 +82,41 @@ contract PartyPool is PartyPoolBase, IPartyPool {
/// @param symbol_ LP token symbol
/// @param tokens_ token addresses (n)
/// @param bases_ scaling bases for each token (n) - used when converting to/from internal 64.64 amounts
/// @param _kappa liquidity parameter κ (Q64.64) used to derive b = κ * S(q)
/// @param _swapFeePpm fee in parts-per-million, taken from swap input amounts before LMSR calculations
/// @param _flashFeePpm fee in parts-per-million, taken for flash loans
/// @param _stable if true and assets.length==2, then the optimization for 2-asset stablecoin pools is activated.
/// @param _swapMintImpl address of the SwapMint implementation contract
/// @param kappa_ liquidity parameter κ (Q64.64) used to derive b = κ * S(q)
/// @param swapFeePpm_ fee in parts-per-million, taken from swap input amounts before LMSR calculations
/// @param flashFeePpm_ fee in parts-per-million, taken for flash loans
/// @param stable_ if true and assets.length==2, then the optimization for 2-asset stablecoin pools is activated.
/// @param swapMintImpl_ address of the SwapMint implementation contract
/// @param mintImpl_ address of the Mint implementation contract
constructor(
string memory name_,
string memory symbol_,
IERC20[] memory tokens_,
uint256[] memory bases_,
int128 _kappa,
uint256 _swapFeePpm,
uint256 _flashFeePpm,
bool _stable,
PartyPoolSwapMintImpl _swapMintImpl
int128 kappa_,
uint256 swapFeePpm_,
uint256 flashFeePpm_,
uint256 protocolFeePpm_, // NEW: protocol share of fees (ppm)
address protocolFeeAddress_, // NEW: recipient for collected protocol tokens
bool stable_,
PartyPoolSwapMintImpl swapMintImpl_,
PartyPoolMintImpl mintImpl_
) PartyPoolBase(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;
require(_flashFeePpm < 1_000_000, "Pool: flash fee >= ppm");
flashFeePpm = _flashFeePpm;
_stablePair = _stable && tokens_.length == 2;
require(address(_swapMintImpl) != address(0), "Pool: impl address zero");
swapMintImpl = address(_swapMintImpl);
KAPPA = kappa_;
require(swapFeePpm_ < 1_000_000, "Pool: fee >= ppm");
SWAP_FEE_PPM = swapFeePpm_;
require(flashFeePpm_ < 1_000_000, "Pool: flash fee >= ppm");
FLASH_FEE_PPM = flashFeePpm_;
require(protocolFeePpm_ < 1_000_000, "Pool: protocol fee >= ppm");
PROTOCOL_FEE_PPM = protocolFeePpm_;
PROTOCOL_FEE_ADDRESS = protocolFeeAddress_;
IS_STABLE_PAIR = stable_ && tokens_.length == 2;
SWAP_MINT_IMPL = swapMintImpl_;
MINT_IMPL = mintImpl_;
uint256 n = tokens_.length;
@@ -104,8 +129,9 @@ contract PartyPool is PartyPoolBase, IPartyPool {
unchecked {i++;}
}
// Initialize caches to zero
// Initialize caches to zero and protocol ledger
cachedUintBalances = new uint256[](n);
protocolFeesOwed = new uint256[](n);
}
@@ -114,240 +140,64 @@ contract PartyPool is PartyPoolBase, IPartyPool {
---------------------- */
/// @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;
}
/// @notice Initial mint to set up pool for the first time.
/// @dev Assumes tokens have already been transferred to the pool prior to calling.
/// Can only be called when the pool is uninitialized (totalSupply() == 0 or lmsr.nAssets == 0).
/// @param receiver address that receives the LP tokens
/// @param lpTokens The number of LP tokens to issue for this mint. If 0, then the number of tokens returned will equal the LMSR internal q total
function initialMint(address receiver, uint256 lpTokens) external nonReentrant
returns (uint256 lpMinted) {
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++; }
bytes memory data = abi.encodeWithSignature(
"initialMint(address,uint256,int128)",
receiver,
lpTokens,
KAPPA
);
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
return abi.decode(result, (uint256));
}
// 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");
_mint(receiver, lpMinted);
emit Mint(address(0), receiver, depositAmounts, lpMinted);
/// @inheritdoc IPartyPool
function mintAmounts(uint256 lpTokenAmount) public view returns (uint256[] memory depositAmounts) {
return MINT_IMPL.mintAmounts(lpTokenAmount, lmsr.nAssets, totalSupply(), cachedUintBalances);
}
/// @notice Proportional mint for existing pool.
/// @dev Payer must approve the required token amounts before calling.
/// Can only be called when pool is already initialized (totalSupply() > 0 and lmsr.nAssets > 0).
/// Rounds follow the pool-favorable conventions documented in helpers (ceil inputs, floor outputs).
/// @dev This function forwards the call to the mint implementation via delegatecall
/// @param payer address that provides the input tokens
/// @param receiver address that receives the LP tokens
/// @param lpTokenAmount desired amount of LP tokens to mint
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external nonReentrant
returns (uint256 lpMinted) {
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;
bytes memory data = abi.encodeWithSignature(
"mint(address,address,uint256,uint256)",
payer,
receiver,
lpTokenAmount,
deadline
);
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
return abi.decode(result, (uint256));
}
/// @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;
function burnAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts) {
return MINT_IMPL.burnAmounts(lpTokenAmount, lmsr.nAssets, totalSupply(), cachedUintBalances);
}
/// @notice Burn LP tokens and withdraw the proportional basket to receiver.
/// @dev Payer must own or approve the LP tokens being burned. The function updates LMSR state
/// proportionally to reflect the reduced pool size after the withdrawal.
/// @dev This function forwards the call to the burn implementation via delegatecall
/// @param payer address that provides the LP tokens to burn
/// @param receiver address that receives the withdrawn tokens
/// @param lpAmount amount of LP tokens to burn (proportional withdrawal)
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external nonReentrant {
require(deadline == 0 || block.timestamp <= deadline, "burn: deadline exceeded");
uint256 n = tokens.length;
require(lpAmount > 0, "burn: zero lp");
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)
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);
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external nonReentrant
returns (uint256[] memory withdrawAmounts) {
bytes memory data = abi.encodeWithSignature(
"burn(address,address,uint256,uint256)",
payer,
receiver,
lpAmount,
deadline
);
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
return abi.decode(result, (uint256[]));
}
/* ----------------------
@@ -376,6 +226,36 @@ contract PartyPool is PartyPoolBase, IPartyPool {
}
// Per-token owed protocol fees (raw token units). Public getter autogenerated.
uint256[] public protocolFeesOwed;
/// @notice Transfer all protocol fees to the configured protocolFeeAddress and zero the ledger.
/// @dev Anyone can call; must have protocolFeeAddress != address(0) to be operational.
function collectProtocolFees() external nonReentrant {
address dest = PROTOCOL_FEE_ADDRESS;
require(dest != address(0), "collect: zero addr");
uint256 n = tokens.length;
for (uint256 i = 0; i < n; i++) {
uint256 owed = protocolFeesOwed[i];
if (owed == 0) continue;
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
require(bal >= owed, "collect: fee > bal");
protocolFeesOwed[i] = 0;
// transfer owed tokens to protocol destination
tokens[i].safeTransfer(dest, owed);
// update cached to effective onchain minus owed
cachedUintBalances[i] = bal - owed;
}
}
/// @dev Helper to record cached balances as effectiveBalance = onchain - owed. Reverts if owed > onchain.
function _recordCachedBalance(uint256 idx, uint256 onchainBal) internal {
uint256 owed = protocolFeesOwed[idx];
require(onchainBal >= owed, "balance < protocol owed");
cachedUintBalances[idx] = onchainBal - owed;
}
/// @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.
@@ -419,9 +299,17 @@ contract PartyPool is PartyPoolBase, IPartyPool {
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;
// Accrue protocol share (floor) from the fee on input token
if (PROTOCOL_FEE_PPM > 0 && feeUint > 0 && PROTOCOL_FEE_ADDRESS != address(0)) {
uint256 protoShare = (feeUint * PROTOCOL_FEE_PPM) / 1_000_000; // floor
if (protoShare > 0) {
protocolFeesOwed[inputTokenIndex] += protoShare;
}
}
// Update cached uint balances for i and j using effective balances (onchain - owed)
_recordCachedBalance(inputTokenIndex, balIAfter);
_recordCachedBalance(outputTokenIndex, balJAfter);
// Apply swap to LMSR state with the internal amounts actually used
lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalUsed, amountOutInternal);
@@ -466,9 +354,17 @@ contract PartyPool is PartyPoolBase, IPartyPool {
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;
// Accrue protocol share (floor) from the fee on input token
if (PROTOCOL_FEE_PPM > 0 && feeUint > 0 && PROTOCOL_FEE_ADDRESS != address(0)) {
uint256 protoShare = (feeUint * PROTOCOL_FEE_PPM) / 1_000_000; // floor
if (protoShare > 0) {
protocolFeesOwed[inputTokenIndex] += protoShare;
}
}
// Update caches to effective balances
_recordCachedBalance(inputTokenIndex, balIAfter);
_recordCachedBalance(outputTokenIndex, balJAfter);
// Apply swap to LMSR state with the internal amounts
lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalMax, amountOutInternal);
@@ -507,7 +403,7 @@ contract PartyPool is PartyPoolBase, IPartyPool {
require(lmsr.nAssets > 0, "swap: empty pool");
// Estimate max net input (fee on gross rounded up, then subtract)
(, uint256 netUintForSwap) = _computeFee(maxAmountIn, swapFeePpm);
(, uint256 netUintForSwap) = _computeFee(maxAmountIn, SWAP_FEE_PPM);
// Convert to internal (floor)
int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, bases[inputTokenIndex]);
@@ -515,9 +411,9 @@ contract PartyPool is PartyPoolBase, IPartyPool {
// Compute internal amounts using LMSR (exact-input with price limit)
// if _stablePair is true, use the optimized path
console2.log('stablepair optimization?', _stablePair);
console2.log('stablepair optimization?', IS_STABLE_PAIR);
(amountInInternalUsed, amountOutInternal) =
_stablePair ? LMSRStabilizedBalancedPair.swapAmountsForExactInput(lmsr, inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice)
IS_STABLE_PAIR ? LMSRStabilizedBalancedPair.swapAmountsForExactInput(lmsr, inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice)
: lmsr.swapAmountsForExactInput(inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice);
// Convert actual used input internal -> uint (ceil)
@@ -527,8 +423,8 @@ contract PartyPool is PartyPoolBase, IPartyPool {
// Compute gross transfer including fee on the used input (ceil)
feeUint = 0;
grossIn = amountInUintNoFee;
if (swapFeePpm > 0) {
feeUint = _ceilFee(amountInUintNoFee, swapFeePpm);
if (SWAP_FEE_PPM > 0) {
feeUint = _ceilFee(amountInUintNoFee, SWAP_FEE_PPM);
grossIn += feeUint;
}
@@ -575,8 +471,8 @@ contract PartyPool is PartyPoolBase, IPartyPool {
feeUint = 0;
grossIn = amountInUintNoFee;
if (swapFeePpm > 0) {
feeUint = _ceilFee(amountInUintNoFee, swapFeePpm);
if (SWAP_FEE_PPM > 0) {
feeUint = _ceilFee(amountInUintNoFee, SWAP_FEE_PPM);
grossIn += feeUint;
}
@@ -587,25 +483,43 @@ contract PartyPool is PartyPoolBase, IPartyPool {
/// @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) {
if (SWAP_FEE_PPM == 0) {
return (0, gross);
}
feeUint = _ceilFee(gross, swapFeePpm);
feeUint = _ceilFee(gross, SWAP_FEE_PPM);
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);
if (SWAP_FEE_PPM == 0) return netUint;
uint256 fee = _ceilFee(netUint, SWAP_FEE_PPM);
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.
function swapMintAmounts(uint256 inputTokenIndex, uint256 maxAmountIn) external view
returns (uint256 amountInUsed, uint256 fee, uint256 lpMinted) {
return SWAP_MINT_IMPL.swapMintAmounts(
inputTokenIndex,
maxAmountIn,
SWAP_FEE_PPM,
lmsr,
bases,
totalSupply()
);
}
// todo swapMintAmounts and burnSwapAmounts
function burnSwapAmounts(uint256 lpAmount, uint256 inputTokenIndex) external view
returns (uint256 amountOut) {
return SWAP_MINT_IMPL.burnSwapAmounts(
lpAmount,
inputTokenIndex,
SWAP_FEE_PPM,
lmsr,
bases,
totalSupply()
);
}
/// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP.
/// @dev This function forwards the call to the swapMint implementation via delegatecall
@@ -629,11 +543,26 @@ contract PartyPool is PartyPoolBase, IPartyPool {
inputTokenIndex,
maxAmountIn,
deadline,
swapFeePpm
SWAP_FEE_PPM
);
bytes memory result = Address.functionDelegateCall(swapMintImpl, data);
return abi.decode(result, (uint256));
bytes memory result = Address.functionDelegateCall(address(SWAP_MINT_IMPL), data);
// New ABI: implementation returns (uint256 lpMinted, uint256 feeUintActual)
(uint256 lpOut, uint256 feeUintActual) = abi.decode(result, (uint256, uint256));
// Accrue protocol share (floor) from the fee on the input token
if (PROTOCOL_FEE_PPM > 0 && feeUintActual > 0 && PROTOCOL_FEE_ADDRESS != address(0)) {
uint256 protoShare = (feeUintActual * PROTOCOL_FEE_PPM) / 1_000_000;
if (protoShare > 0) {
protocolFeesOwed[inputTokenIndex] += protoShare;
}
}
// Update cached balance for the input token to effective onchain - owed
uint256 bal = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
_recordCachedBalance(inputTokenIndex, bal);
return lpOut;
}
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver.
@@ -658,11 +587,26 @@ contract PartyPool is PartyPoolBase, IPartyPool {
lpAmount,
inputTokenIndex,
deadline,
swapFeePpm
SWAP_FEE_PPM
);
bytes memory result = Address.functionDelegateCall(swapMintImpl, data);
return abi.decode(result, (uint256));
bytes memory result = Address.functionDelegateCall(address(SWAP_MINT_IMPL), data);
// New ABI: implementation returns (uint256 amountOutUint, uint256 feeTokenUint)
(uint256 outAmt, uint256 feeTokenUint) = abi.decode(result, (uint256, uint256));
// Accrue protocol share (floor) from the token-side fee computed by implementation
if (PROTOCOL_FEE_PPM > 0 && feeTokenUint > 0 && PROTOCOL_FEE_ADDRESS != address(0)) {
uint256 protoShare = (feeTokenUint * PROTOCOL_FEE_PPM) / 1_000_000;
if (protoShare > 0) {
protocolFeesOwed[inputTokenIndex] += protoShare;
}
}
// Update cached balance for the target token to effective onchain - owed
uint256 bal = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
_recordCachedBalance(inputTokenIndex, bal);
return outAmt;
}
@@ -673,7 +617,7 @@ contract PartyPool is PartyPoolBase, IPartyPool {
for (uint256 i = 0; i < tokens.length; i++) {
uint256 amount = loanAmounts[i];
if (amount > 0) {
repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm);
repaymentAmounts[i] = amount + _ceilFee(amount, FLASH_FEE_PPM);
}
}
}
@@ -713,7 +657,7 @@ contract PartyPool is PartyPoolBase, IPartyPool {
hasNonZeroAmount = true;
// Calculate repayment amount with fee (ceiling)
repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm);
repaymentAmounts[i] = amount + _ceilFee(amount, FLASH_FEE_PPM);
// Record initial balance
initialBalances[i] = IERC20(tokens[i]).balanceOf(address(this));
@@ -734,14 +678,25 @@ contract PartyPool is PartyPoolBase, IPartyPool {
if (amounts[i] > 0) {
uint256 currentBalance = IERC20(tokens[i]).balanceOf(address(this));
// Compute expected fee (ceiling)
uint256 feeExpected = _ceilFee(amounts[i], FLASH_FEE_PPM);
// Verify repayment: current balance must be at least (initial balance + fee)
require(
currentBalance >= initialBalances[i] + _ceilFee(amounts[i], flashFeePpm),
currentBalance >= initialBalances[i] + feeExpected,
"flash: repayment failed"
);
// Update cached balance
cachedUintBalances[i] = currentBalance;
// Accrue protocol share (floor) of the flash fee
if (PROTOCOL_FEE_PPM > 0 && PROTOCOL_FEE_ADDRESS != address(0)) {
uint256 protoShare = (feeExpected * PROTOCOL_FEE_PPM) / 1_000_000; // floor
if (protoShare > 0) {
protocolFeesOwed[i] += protoShare;
}
}
// Update cached balance to onchain minus owed
_recordCachedBalance(i, currentBalance);
}
}
}

237
src/PartyPoolMintImpl.sol Normal file
View File

@@ -0,0 +1,237 @@
// 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 "./PartyPoolBase.sol";
import "./LMSRStabilized.sol";
import {PartyPool} from "./PartyPool.sol";
/// @title PartyPoolMintImpl - Implementation contract for mint and burn functions
/// @notice This contract contains the mint and burn implementation that will be called via delegatecall
/// @dev This contract inherits from PartyPoolBase to access storage and internal functions
contract PartyPoolMintImpl is PartyPoolBase {
using ABDKMath64x64 for int128;
using LMSRStabilized for LMSRStabilized.State;
using SafeERC20 for IERC20;
// 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);
constructor() PartyPoolBase('','') {}
function initialMint(address receiver, uint256 lpTokens, int128 KAPPA) external
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");
_mint(receiver, lpMinted);
emit Mint(address(0), receiver, depositAmounts, lpMinted);
}
function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external 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 = mintAmounts(lpTokenAmount, lmsr.nAssets, totalSupply(), cachedUintBalances);
// 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;
}
/// @notice Burn LP tokens and withdraw the proportional basket to receiver.
/// @dev Payer must own or approve the LP tokens being burned. The function updates LMSR state
/// proportionally to reflect the reduced pool size after the withdrawal.
/// @param payer address that provides the LP tokens to burn
/// @param receiver address that receives the withdrawn tokens
/// @param lpAmount amount of LP tokens to burn (proportional withdrawal)
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external
returns (uint256[] memory withdrawAmounts) {
require(deadline == 0 || block.timestamp <= deadline, "burn: deadline exceeded");
uint256 n = tokens.length;
require(lpAmount > 0, "burn: zero lp");
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)
withdrawAmounts = burnAmounts(lpAmount, lmsr.nAssets, totalSupply(), cachedUintBalances);
// 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)
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);
}
function mintAmounts(uint256 lpTokenAmount, uint256 numAssets, uint256 totalSupply, uint256[] memory cachedUintBalances) public pure
returns (uint256[] memory depositAmounts) {
depositAmounts = new uint256[](numAssets);
// 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 || numAssets == 0) {
return depositAmounts; // Return zeros, initial deposit handled differently
}
// lpTokenAmount / totalLpSupply = depositAmount / currentBalance
// Therefore: depositAmount = (lpTokenAmount * currentBalance) / totalLpSupply
// We round up to protect the pool
for (uint i = 0; i < numAssets; i++) {
uint256 currentBalance = cachedUintBalances[i];
// Calculate with rounding up: (a * b + c - 1) / c
depositAmounts[i] = (lpTokenAmount * currentBalance + totalSupply - 1) / totalSupply;
}
return depositAmounts;
}
function burnAmounts(uint256 lpTokenAmount,
uint256 numAssets, uint256 totalSupply, uint256[] memory cachedUintBalances) public pure
returns (uint256[] memory withdrawAmounts) {
withdrawAmounts = new uint256[](numAssets);
// If supply is zero or pool uninitialized, return zeros
if (totalSupply == 0 || numAssets == 0) {
return withdrawAmounts; // Return zeros, nothing to withdraw
}
// withdrawAmount = floor(lpTokenAmount * currentBalance / totalLpSupply)
for (uint i = 0; i < numAssets; i++) {
uint256 currentBalance = cachedUintBalances[i];
withdrawAmounts[i] = (lpTokenAmount * currentBalance) / totalSupply;
}
return withdrawAmounts;
}
}

View File

@@ -40,13 +40,11 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
uint256 maxAmountIn,
uint256 deadline,
uint256 swapFeePpm
) external returns (uint256 lpMinted) {
) external returns (uint256 lpMinted, uint256 feeUintActual) {
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)
@@ -64,7 +62,7 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
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);
feeUintActual = _ceilFee(amountInUint, swapFeePpm);
uint256 totalTransfer = amountInUint + feeUintActual;
require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMint: transfer exceeds max");
@@ -74,7 +72,7 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
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)
// Update cached uint balances for token inputTokenIndex (implementation writes onchain value; wrapper will set effective)
cachedUintBalances[inputTokenIndex] = balIAfter;
// Compute old and new scaled size metrics to determine LP minted
@@ -124,7 +122,119 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
emit Mint(payer, receiver, new uint256[](n), actualLpToMint);
// Note: depositAmounts array omitted (empty) since swapMint uses single-token input
return actualLpToMint;
lpMinted = actualLpToMint;
return (lpMinted, feeUintActual);
}
/// @notice Calculate the amounts for a swap mint operation
/// @dev This is a pure view function that computes swap mint amounts from provided state
/// @param inputTokenIndex index of the input token
/// @param maxAmountIn maximum amount of token to deposit (inclusive of fee)
/// @param swapFeePpm fee in parts-per-million
/// @param lmsrState current LMSR state
/// @param bases_ scaling bases for each token
/// @param totalSupply_ current total LP token supply
/// @return amountInUsed actual input amount used (excluding fee)
/// @return fee fee amount charged
/// @return lpMinted LP tokens that would be minted
function swapMintAmounts(
uint256 inputTokenIndex,
uint256 maxAmountIn,
uint256 swapFeePpm,
LMSRStabilized.State memory lmsrState,
uint256[] memory bases_,
uint256 totalSupply_
) public pure returns (uint256 amountInUsed, uint256 fee, uint256 lpMinted) {
require(inputTokenIndex < bases_.length, "swapMintAmounts: idx");
require(maxAmountIn > 0, "swapMintAmounts: input zero");
require(lmsrState.nAssets > 0, "swapMintAmounts: uninit pool");
// Compute fee on gross maxAmountIn to get an initial net estimate
uint256 feeGuess = 0;
uint256 netUintGuess = maxAmountIn;
if (swapFeePpm > 0) {
feeGuess = (maxAmountIn * swapFeePpm + 999999) / 1000000; // ceil fee
netUintGuess = maxAmountIn - feeGuess;
}
// Convert the net guess to internal (floor)
int128 netInternalGuess = _uintToInternalFloorPure(netUintGuess, bases_[inputTokenIndex]);
require(netInternalGuess > int128(0), "swapMintAmounts: input too small after fee");
// Use LMSR view to determine actual internal consumed and size-increase (ΔS) for mint
(int128 amountInInternalUsed, int128 sizeIncreaseInternal) =
LMSRStabilized.swapAmountsForMint(lmsrState.nAssets, lmsrState.kappa, lmsrState.qInternal,
inputTokenIndex, netInternalGuess);
// amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer
amountInUsed = _internalToUintCeilPure(amountInInternalUsed, bases_[inputTokenIndex]);
require(amountInUsed > 0, "swapMintAmounts: input zero after internal conversion");
// Compute fee on the actual used input (ceiling)
fee = 0;
if (swapFeePpm > 0) {
fee = (amountInUsed * swapFeePpm + 999999) / 1000000; // ceil fee
}
uint256 totalTransfer = amountInUsed + fee;
require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMintAmounts: transfer exceeds max");
// Compute old and new scaled size metrics to determine LP minted
int128 oldTotal = _computeSizeMetricPure(lmsrState.qInternal);
require(oldTotal > int128(0), "swapMintAmounts: zero total");
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
int128 newTotal = oldTotal.add(sizeIncreaseInternal);
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
if (totalSupply_ == 0) {
// If somehow supply zero (shouldn't happen as lmsr.nAssets>0), mint newScaled
lpMinted = newScaled;
} else {
require(oldScaled > 0, "swapMintAmounts: oldScaled zero");
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
if (delta > 0) {
// floor truncation rounds in favor of pool
lpMinted = (totalSupply_ * delta) / oldScaled;
} else {
lpMinted = 0;
}
}
require(lpMinted > 0, "swapMintAmounts: zero LP minted");
}
/// @notice Calculate the amounts for a burn swap operation
/// @dev This is a pure view function that computes burn swap amounts from provided state
/// @param lpAmount amount of LP tokens to burn
/// @param inputTokenIndex index of target asset to receive
/// @param swapFeePpm fee in parts-per-million
/// @param lmsrState current LMSR state
/// @param bases_ scaling bases for each token
/// @param totalSupply_ current total LP token supply
/// @return amountOut amount of target asset that would be received
function burnSwapAmounts(
uint256 lpAmount,
uint256 inputTokenIndex,
uint256 swapFeePpm,
LMSRStabilized.State memory lmsrState,
uint256[] memory bases_,
uint256 totalSupply_
) public pure returns (uint256 amountOut) {
require(inputTokenIndex < bases_.length, "burnSwapAmounts: idx");
require(lpAmount > 0, "burnSwapAmounts: zero lp");
require(totalSupply_ > 0, "burnSwapAmounts: empty supply");
// alpha = lpAmount / supply as Q64.64
int128 alpha = ABDKMath64x64.divu(lpAmount, totalSupply_) // fraction of total supply to burn
.mul(ABDKMath64x64.divu(1000000-swapFeePpm, 1000000)); // adjusted for fee
// Use LMSR view to compute single-asset payout and burned size-metric
(int128 payoutInternal, ) = LMSRStabilized.swapAmountsForBurn(lmsrState.nAssets, lmsrState.kappa, lmsrState.qInternal,
inputTokenIndex, alpha);
// Convert payoutInternal -> uint (floor) to favor pool
amountOut = _internalToUintFloorPure(payoutInternal, bases_[inputTokenIndex]);
require(amountOut > 0, "burnSwapAmounts: output zero");
}
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver.
@@ -136,7 +246,6 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
/// @param deadline optional deadline
/// @param swapFeePpm fee in parts-per-million for this pool (may be used for future fee logic)
/// @return amountOutUint uint amount of asset i sent to receiver
// todo fee!?
function burnSwap(
address payer,
address receiver,
@@ -144,7 +253,7 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
uint256 inputTokenIndex,
uint256 deadline,
uint256 swapFeePpm
) external returns (uint256 amountOutUint) {
) external returns (uint256 amountOutUint, uint256 feeTokenUint) {
uint256 n = tokens.length;
require(inputTokenIndex < n, "burnSwap: idx");
require(lpAmount > 0, "burnSwap: zero lp");
@@ -154,8 +263,9 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
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);
// alpha = lpAmount / supply as Q64.64 (adjusted for fee)
int128 alpha = ABDKMath64x64.divu(lpAmount, supply) // fraction of total supply to burn
.mul(ABDKMath64x64.divu(1000000-swapFeePpm, 1000000)); // adjusted for fee
// Use LMSR view to compute single-asset payout and burned size-metric
(int128 payoutInternal, ) = lmsr.swapAmountsForBurn(inputTokenIndex, alpha);
@@ -164,6 +274,12 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
amountOutUint = _internalToUintFloor(payoutInternal, bases[inputTokenIndex]);
require(amountOutUint > 0, "burnSwap: output zero");
// Compute gross payout (no swap fee) so we can determine token-side fee = gross - net
int128 alphaGross = ABDKMath64x64.divu(lpAmount, supply); // gross fraction (no swap fee)
(int128 payoutGrossInternal, ) = lmsr.swapAmountsForBurn(inputTokenIndex, alphaGross);
uint256 payoutGrossUint = _internalToUintFloor(payoutGrossInternal, bases[inputTokenIndex]);
feeTokenUint = (payoutGrossUint > amountOutUint) ? (payoutGrossUint - amountOutUint) : 0;
// Transfer the payout to receiver
tokens[inputTokenIndex].safeTransfer(receiver, amountOutUint);
@@ -179,6 +295,7 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
int128[] memory newQInternal = new int128[](n);
for (uint256 idx = 0; idx < n; idx++) {
uint256 bal = IERC20(tokens[idx]).balanceOf(address(this));
// implementation writes raw onchain values; wrapper will set effective cached values
cachedUintBalances[idx] = bal;
newQInternal[idx] = _uintToInternalFloor(bal, bases[idx]);
}
@@ -198,6 +315,42 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
}
emit Burn(payer, receiver, new uint256[](n), lpAmount);
return amountOutUint;
return (amountOutUint, feeTokenUint);
}
/// @notice Pure version of _uintToInternalFloor for use in view functions
function _uintToInternalFloorPure(uint256 amount, uint256 base) internal pure returns (int128) {
// amount / base as Q64.64, floored
return ABDKMath64x64.divu(amount, base);
}
/// @notice Pure version of _internalToUintCeil for use in view functions
function _internalToUintCeilPure(int128 amount, uint256 base) internal pure returns (uint256) {
// Convert Q64.64 to uint with ceiling: ceil(amount * base)
// Use mulu which floors, then add remainder check for ceiling
uint256 floored = ABDKMath64x64.mulu(amount, base);
// Check if there's a fractional part by computing amount * base - floored
int128 baseQ64 = ABDKMath64x64.fromUInt(base);
int128 flooredQ64 = ABDKMath64x64.fromUInt(floored);
int128 product = amount.mul(baseQ64);
if (product > flooredQ64) {
return floored + 1; // Ceiling
}
return floored;
}
/// @notice Pure version of _internalToUintFloor for use in view functions
function _internalToUintFloorPure(int128 amount, uint256 base) internal pure returns (uint256) {
// Convert Q64.64 to uint with floor: floor(amount * base)
return ABDKMath64x64.mulu(amount, base);
}
/// @notice Pure version of _computeSizeMetric for use in view functions
function _computeSizeMetricPure(int128[] memory qInternal) internal pure returns (int128) {
int128 sum = int128(0);
for (uint256 i = 0; i < qInternal.length; i++) {
sum = sum.add(qInternal[i]);
}
return sum;
}
}

View File

@@ -1,5 +1,6 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
/* solhint-disable erc20-unchecked-transfer */
import "forge-std/Test.sol";
import "@abdk/ABDKMath64x64.sol";
@@ -129,22 +130,23 @@ contract TestERC20 is ERC20 {
/// @notice Gas testing contract for PartyPool - contains all gas measurement tests
contract GasTest is Test {
using ABDKMath64x64 for int128;
using SafeERC20 for TestERC20;
PartyPlanner planner;
PartyPool pool2;
PartyPool pool10;
PartyPool pool20;
PartyPool pool50;
PartyPlanner internal planner;
PartyPool internal pool2;
PartyPool internal pool10;
PartyPool internal pool20;
PartyPool internal pool50;
address alice;
address bob;
address internal alice;
address internal bob;
// Common parameters
int128 tradeFrac;
int128 targetSlippage;
int128 internal tradeFrac;
int128 internal targetSlippage;
uint256 constant INIT_BAL = 1_000_000; // initial token units for each token (internal==amount when base==1)
uint256 constant BASE = 1; // use base=1 so internal amounts correspond to raw integers (Q64.64 units)
uint256 constant internal INIT_BAL = 1_000_000; // initial token units for each token (internal==amount when base==1)
uint256 constant internal BASE = 1; // use base=1 so internal amounts correspond to raw integers (Q64.64 units)
/// @notice Helper function to create a pool with the specified number of tokens
function createPool(uint256 numTokens) internal returns (PartyPool) {

View File

@@ -2,7 +2,7 @@
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "forge-std/console.sol";
import "forge-std/console2.sol";
import "@openzeppelin/contracts/interfaces/IERC20Metadata.sol";
import "../src/LMSRStabilized.sol";
import "../src/LMSRStabilizedBalancedPair.sol";
@@ -711,7 +711,7 @@ contract LMSRStabilizedTest is Test {
}
// Path 1: Direct swap from asset 0 to asset 2
(int128 directAmountIn, int128 directAmountOut) = s.swapAmountsForExactInput(0, 2, directSwapAmount, 0);
(/*int128 directAmountIn*/, int128 directAmountOut) = s.swapAmountsForExactInput(0, 2, directSwapAmount, 0);
// Restore original state for second path
_updateCachedQInternal(backupQ);
@@ -724,7 +724,7 @@ contract LMSRStabilizedTest is Test {
s.qInternal[1] = s.qInternal[1].add(indirectAmountOut1);
// Second swap: asset 1 -> asset 2
(int128 indirectAmountIn2, int128 indirectAmountOut2) = s.swapAmountsForExactInput(1, 2, indirectAmountOut1, 0);
(/*int128 indirectAmountIn2*/, int128 indirectAmountOut2) = s.swapAmountsForExactInput(1, 2, indirectAmountOut1, 0);
// The path independence property isn't perfect due to discrete swap mechanics,
// but the difference should be within reasonable bounds
@@ -765,7 +765,7 @@ contract LMSRStabilizedTest is Test {
s.qInternal[1] = s.qInternal[1].add(amountOut1);
// Step 2: Swap back asset 1 -> asset 0
(int128 amountIn2, int128 amountOut2) = s.swapAmountsForExactInput(1, 0, amountOut1, 0);
(/*int128 amountIn2*/, int128 amountOut2) = s.swapAmountsForExactInput(1, 0, amountOut1, 0);
// Calculate round-trip slippage: (initial amount - final amount) / initial amount
int128 roundTripSlippage = (amountIn1.sub(amountOut2)).div(amountIn1);

View File

@@ -4,11 +4,11 @@ pragma solidity ^0.8.30;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockERC20 is ERC20 {
uint8 private immutable _decimals;
uint8 private immutable DECIMALS;
constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) {_decimals = decimals_;}
constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) {DECIMALS = decimals_;}
function decimals() public view virtual override returns (uint8) {return _decimals;}
function decimals() public view virtual override returns (uint8) {return DECIMALS;}
function mint(address account, uint256 amount) external {_mint(account, amount);}
function burn(address account, uint256 amount) external {_burn(account, amount);}
}

View File

@@ -1,4 +1,5 @@
// SPDX-License-Identifier: UNLICENSED
/* solhint-disable */
pragma solidity ^0.8.30;
import "forge-std/Test.sol";
@@ -323,7 +324,7 @@ contract PartyPoolTest is Test {
token2.approve(address(pool), type(uint256).max);
// Inspect the deposit amounts that the pool will require (these are rounded up)
uint256[] memory deposits = pool.mintDepositAmounts(1);
uint256[] memory deposits = pool.mintAmounts(1);
// Basic sanity: deposits array length must match token count and not all zero necessarily
assertEq(deposits.length, 3);
@@ -365,7 +366,7 @@ contract PartyPoolTest is Test {
uint256 totalLpBefore = pool.totalSupply();
// Compute required deposits and perform mint for 1 wei
uint256[] memory deposits = pool.mintDepositAmounts(1);
uint256[] memory deposits = pool.mintAmounts(1);
// Sum deposits as deposited_value
uint256 depositedValue = 0;
@@ -399,14 +400,14 @@ contract PartyPoolTest is Test {
vm.stopPrank();
}
/// @notice mintDepositAmounts should round up deposit amounts to protect the pool.
/// @notice mintAmounts should round up deposit amounts to protect the pool.
function testMintDepositAmountsRoundingUp() public view {
uint256 totalLp = pool.totalSupply();
assertTrue(totalLp > 0, "precondition: total supply > 0");
// Request half of LP supply
uint256 want = totalLp / 2;
uint256[] memory deposits = pool.mintDepositAmounts(want);
uint256[] memory deposits = pool.mintAmounts(want);
// We expect each deposit to be roughly half the pool balance, but due to rounding up it should satisfy:
// deposits[i] * 2 >= cached balance (i.e., rounding up)
@@ -423,7 +424,7 @@ contract PartyPoolTest is Test {
assertTrue(totalLp > 0, "precondition: LP > 0");
// Compute amounts required to redeem entire supply (should be current balances)
uint256[] memory withdrawAmounts = pool.burnReceiveAmounts(totalLp);
uint256[] memory withdrawAmounts = pool.burnAmounts(totalLp);
// Sanity: withdrawAmounts should equal pool balances (or very close due to rounding)
for (uint i = 0; i < withdrawAmounts.length; i++) {
@@ -513,7 +514,7 @@ contract PartyPoolTest is Test {
}
/// @notice Verify mintDepositAmounts matches the actual token transfers performed by mint()
/// @notice Verify mintAmounts matches the actual token transfers performed by mint()
function testMintDepositAmountsMatchesMint_3TokenPool() public {
// Use a range of LP requests (tiny to large fraction)
uint256 totalLp = pool.totalSupply();
@@ -527,7 +528,7 @@ contract PartyPoolTest is Test {
if (req == 0) req = 1;
// Compute expected deposit amounts via view
uint256[] memory expected = pool.mintDepositAmounts(req);
uint256[] memory expected = pool.mintAmounts(req);
// Ensure alice has tokens and approve pool
vm.startPrank(alice);
@@ -541,7 +542,7 @@ contract PartyPoolTest is Test {
uint256 a2Before = token2.balanceOf(alice);
// Perform mint (may revert for zero-request; ensure req>0 above)
// Guard: if mintDepositAmounts returned all zeros, skip (nothing to transfer)
// Guard: if mintAmounts returned all zeros, skip (nothing to transfer)
bool allZero = (expected[0] == 0 && expected[1] == 0 && expected[2] == 0);
if (!allZero) {
uint256 lpBefore = pool.balanceOf(alice);
@@ -560,7 +561,7 @@ contract PartyPoolTest is Test {
}
}
/// @notice Verify mintDepositAmounts matches the actual token transfers performed by mint() for 10-token pool
/// @notice Verify mintAmounts matches the actual token transfers performed by mint() for 10-token pool
function testMintDepositAmountsMatchesMint_10TokenPool() public {
uint256 totalLp = pool10.totalSupply();
uint256[] memory requests = new uint256[](4);
@@ -572,7 +573,7 @@ contract PartyPoolTest is Test {
uint256 req = requests[k];
if (req == 0) req = 1;
uint256[] memory expected = pool10.mintDepositAmounts(req);
uint256[] memory expected = pool10.mintAmounts(req);
// Approve all tokens from alice
vm.startPrank(alice);
@@ -623,7 +624,7 @@ contract PartyPoolTest is Test {
}
}
/// @notice Verify burnReceiveAmounts matches actual transfers performed by burn() for 3-token pool
/// @notice Verify burnAmounts matches actual transfers performed by burn() for 3-token pool
function testBurnReceiveAmountsMatchesBurn_3TokenPool() public {
// Use address(this) as payer (holds initial LP from setUp)
uint256 totalLp = pool.totalSupply();
@@ -650,7 +651,7 @@ contract PartyPoolTest is Test {
}
// Recompute withdraw amounts via view after any top-up
uint256[] memory expected = pool.burnReceiveAmounts(req);
uint256[] memory expected = pool.burnAmounts(req);
// If expected withdraws are all zero (rounding edge), skip this iteration
if (expected[0] == 0 && expected[1] == 0 && expected[2] == 0) {
@@ -676,7 +677,7 @@ contract PartyPoolTest is Test {
}
}
/// @notice Verify burnReceiveAmounts matches actual transfers performed by burn() for 10-token pool
/// @notice Verify burnAmounts matches actual transfers performed by burn() for 10-token pool
function testBurnReceiveAmountsMatchesBurn_10TokenPool() public {
uint256 totalLp = pool10.totalSupply();
uint256[] memory burns = new uint256[](4);
@@ -707,7 +708,7 @@ contract PartyPoolTest is Test {
vm.stopPrank();
}
uint256[] memory expected = pool10.burnReceiveAmounts(req);
uint256[] memory expected = pool10.burnAmounts(req);
// If expected withdraws are all zero (rounding edge), skip this iteration
bool allZero = true;
@@ -1360,8 +1361,8 @@ contract PartyPoolTest is Test {
token2.approve(address(poolCustom), type(uint256).max);
// Get required deposit amounts for both pools
uint256[] memory depositsDefault = poolDefault.mintDepositAmounts(lpRequestDefault);
uint256[] memory depositsCustom = poolCustom.mintDepositAmounts(lpRequestCustom);
uint256[] memory depositsDefault = poolDefault.mintAmounts(lpRequestDefault);
uint256[] memory depositsCustom = poolCustom.mintAmounts(lpRequestCustom);
// Deposits should be identical (same proportion of identical balances)
assertEq(depositsDefault[0], depositsCustom[0], "Token0 deposits should be identical");
@@ -1389,3 +1390,4 @@ contract PartyPoolTest is Test {
}
}
/* solhint-enable */