Compare commits

5 Commits

Author SHA1 Message Date
tim
43fb62c47c Deploy; PartyPoolBalancedPair as separate contract 2025-09-29 18:05:04 -04:00
tim
a43c893609 PoolBase; warnings cleanup 2025-09-29 17:32:09 -04:00
tim
e5b2577ba9 swapMintAmounts 2025-09-29 17:17:23 -04:00
tim
8e69bfac5c PoolLib 2025-09-29 17:07:55 -04:00
tim
77683555e8 removed console logs 2025-09-29 16:40:39 -04:00
14 changed files with 1282 additions and 975 deletions

View File

@@ -9,7 +9,7 @@ remappings = [
optimizer=true optimizer=true
optimizer_runs=999999999 optimizer_runs=999999999
viaIR=true viaIR=true
gas_reports = ['PartyPool', 'PartyPlanner'] gas_reports = ['PartyPlanner', 'PartyPoolBalancedPair', 'PartyPool', ]
fs_permissions = [{ access = "write", path = "chain.json"}] fs_permissions = [{ access = "write", path = "chain.json"}]
[lint] [lint]

30
src/Deploy.sol Normal file
View File

@@ -0,0 +1,30 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import "./PartyPoolBalancedPair.sol";
import {PartyPlanner} from "./PartyPlanner.sol";
import {PartyPool} from "./PartyPool.sol";
library Deploy {
function newPartyPlanner() internal returns (PartyPlanner) {
return new PartyPlanner();
}
function newPartyPool(
string memory name_,
string memory symbol_,
IERC20[] memory tokens_,
uint256[] memory bases_,
int128 kappa_,
uint256 swapFeePpm_,
uint256 flashFeePpm_,
bool stablePair_
) internal returns (PartyPool) {
return stablePair_ ?
new PartyPoolBalancedPair(name_, symbol_, tokens_, bases_, kappa_, swapFeePpm_, flashFeePpm_) :
new PartyPool(name_, symbol_, tokens_, bases_, kappa_, swapFeePpm_, flashFeePpm_);
}
}

View File

@@ -188,6 +188,15 @@ interface IPartyPool is IERC20Metadata {
uint256 deadline uint256 deadline
) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee); ) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee);
/// @notice External view to quote swapMint amounts, matching swapMint() computations
/// @param inputTokenIndex index of input token to deposit
/// @param maxAmountIn maximum gross input allowed (inclusive of fee)
/// @return totalTransfer gross input amount to transfer (includes fee), amountIn net input amount used for minting, fee fee amount taken, lpMinted LP tokens that would be minted
function swapMintAmounts(
uint256 inputTokenIndex,
uint256 maxAmountIn
) external view returns (uint256 totalTransfer, uint256 amountIn, uint256 fee, uint256 lpMinted);
/// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP. /// @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. /// @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. /// The function emits SwapMint (gross, net, fee) and also emits Mint for LP issuance.
@@ -205,6 +214,15 @@ interface IPartyPool is IERC20Metadata {
uint256 deadline uint256 deadline
) external returns (uint256 lpMinted); ) external returns (uint256 lpMinted);
/// @notice External view to quote burnSwap amounts, matching burnSwap() computations
/// @param lpAmount amount of LP tokens to burn
/// @param inputTokenIndex index of target asset to receive
/// @return amountOut output amount user would receive after fees
function burnSwapAmounts(
uint256 lpAmount,
uint256 inputTokenIndex
) external view returns (uint256 amountOut);
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver. /// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver.
/// @dev The function burns LP tokens (authorization via allowance if needed), sends the single-asset payout and updates LMSR state. /// @dev The function burns LP tokens (authorization via allowance if needed), sends the single-asset payout and updates LMSR state.
/// @param payer who burns LP tokens /// @param payer who burns LP tokens

View File

@@ -1,7 +1,6 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30; pragma solidity ^0.8.30;
import "forge-std/console2.sol";
import "@abdk/ABDKMath64x64.sol"; import "@abdk/ABDKMath64x64.sol";
/// @notice Stabilized LMSR library with incremental exp(z) caching for gas efficiency. /// @notice Stabilized LMSR library with incremental exp(z) caching for gas efficiency.
@@ -41,21 +40,11 @@ library LMSRStabilized {
} }
int128 total = _computeSizeMetric(s.qInternal); int128 total = _computeSizeMetric(s.qInternal);
console2.log("total (internal 64.64)");
console2.logInt(total);
require(total > int128(0), "LMSR: total zero"); require(total > int128(0), "LMSR: total zero");
console2.log("LMSR.init: start");
console2.log("nAssets", s.nAssets);
console2.log("qInternal.length", s.qInternal.length);
// Set kappa directly (caller provides kappa) // Set kappa directly (caller provides kappa)
s.kappa = kappa; s.kappa = kappa;
console2.log("kappa (64x64)");
console2.logInt(s.kappa);
require(s.kappa > int128(0), "LMSR: kappa>0"); require(s.kappa > int128(0), "LMSR: kappa>0");
console2.log("LMSR.init: done");
} }
/* -------------------- /* --------------------
@@ -125,49 +114,29 @@ library LMSRStabilized {
// push the marginal price p_i/p_j beyond the limit; if so, truncate `a`. // push the marginal price p_i/p_j beyond the limit; if so, truncate `a`.
// Marginal price ratio evolves as r(t) = r0 * exp(t/b) (since e_i multiplies by exp(t/b)) // Marginal price ratio evolves as r(t) = r0 * exp(t/b) (since e_i multiplies by exp(t/b))
if (limitPrice > int128(0)) { if (limitPrice > int128(0)) {
console2.log("\n=== LimitPrice Logic Debug ===");
console2.log("Received limitPrice (64x64):");
console2.logInt(limitPrice);
console2.log("Current price ratio r0 (e_i/e_j, 64x64):");
console2.logInt(r0);
// r0 must be positive; if r0 == 0 then no risk of exceeding limit by increasing r. // r0 must be positive; if r0 == 0 then no risk of exceeding limit by increasing r.
require(r0 >= int128(0), "LMSR: r0<0"); require(r0 >= int128(0), "LMSR: r0<0");
if (r0 == int128(0)) { if (r0 == int128(0)) {
console2.log("r0 == 0 (input asset has zero weight), no limit truncation needed"); // console2.log("r0 == 0 (input asset has zero weight), no limit truncation needed");
} else { } else {
// If limitPrice <= current price, we revert (caller must choose a limit > current price to allow any fill) // If limitPrice <= current price, we revert (caller must choose a limit > current price to allow any fill)
if (limitPrice <= r0) { require(limitPrice > r0, "LMSR: limitPrice <= current price");
console2.log("Limit price is <= current price: reverting");
revert("LMSR: limitPrice <= current price");
}
// Compute a_limit directly from ln(limit / r0): a_limit = b * ln(limit / r0) // Compute a_limit directly from ln(limit / r0): a_limit = b * ln(limit / r0)
int128 ratioLimitOverR0 = limitPrice.div(r0); int128 ratioLimitOverR0 = limitPrice.div(r0);
console2.log("limitPrice/r0 (64x64):");
console2.logInt(ratioLimitOverR0);
require(ratioLimitOverR0 > int128(0), "LMSR: ratio<=0"); require(ratioLimitOverR0 > int128(0), "LMSR: ratio<=0");
int128 aLimitOverB = _ln(ratioLimitOverR0); // > 0 int128 aLimitOverB = _ln(ratioLimitOverR0); // > 0
console2.log("ln(limitPrice/r0) (64x64):");
console2.logInt(aLimitOverB);
// aLimit = b * aLimitOverB // aLimit = b * aLimitOverB
int128 aLimit64 = b.mul(aLimitOverB); int128 aLimit64 = b.mul(aLimitOverB);
console2.log("aLimit in 64x64 format:");
console2.logInt(aLimit64);
// If computed aLimit is less than the requested a, use the truncated value. // If computed aLimit is less than the requested a, use the truncated value.
if (aLimit64 < a) { if (aLimit64 < a) {
console2.log("TRUNCATING: a reduced from 64.64 value");
console2.logInt(a);
console2.log("to 64.64 value");
console2.logInt(aLimit64);
amountIn = aLimit64; // Store the truncated input amount amountIn = aLimit64; // Store the truncated input amount
a = aLimit64; // Use truncated amount for calculations a = aLimit64; // Use truncated amount for calculations
} else { } else {
console2.log("Not truncating: aLimit64 >= a"); // console2.log("Not truncating: aLimit64 >= a");
} }
} }
} }
@@ -177,56 +146,24 @@ library LMSRStabilized {
// Protect exp from enormous inputs (consistent with recenter thresholds) // Protect exp from enormous inputs (consistent with recenter thresholds)
require(aOverB <= EXP_LIMIT, "LMSR: a/b too large (would overflow exp)"); require(aOverB <= EXP_LIMIT, "LMSR: a/b too large (would overflow exp)");
console2.log("\n=== AmountOut Calculation Debug ===");
console2.log("Input amount (64.64):");
console2.logInt(a);
console2.log("a/b (64x64):");
console2.logInt(aOverB);
// Use the closed-form fee-free formula: // Use the closed-form fee-free formula:
// y = b * ln(1 + r0 * (1 - exp(-a/b))) // y = b * ln(1 + r0 * (1 - exp(-a/b)))
console2.log("r0_for_calc (e_i/e_j):");
console2.logInt(r0);
int128 expNeg = _exp(aOverB.neg()); // exp(-a/b) int128 expNeg = _exp(aOverB.neg()); // exp(-a/b)
console2.log("exp(-a/b):");
console2.logInt(expNeg);
int128 oneMinusExpNeg = ONE.sub(expNeg); int128 oneMinusExpNeg = ONE.sub(expNeg);
console2.log("1 - exp(-a/b):");
console2.logInt(oneMinusExpNeg);
int128 inner = ONE.add(r0.mul(oneMinusExpNeg)); int128 inner = ONE.add(r0.mul(oneMinusExpNeg));
console2.log("inner = 1 + r0 * (1 - exp(-a/b)):");
console2.logInt(inner);
// If inner <= 0 then cap output to the current balance q_j (cannot withdraw more than q_j) // If inner <= 0 then cap output to the current balance q_j (cannot withdraw more than q_j)
if (inner <= int128(0)) { if (inner <= int128(0)) {
console2.log("WARNING: inner <= 0, capping output to balance q_j");
int128 qj64 = s.qInternal[j]; int128 qj64 = s.qInternal[j];
console2.log("Capped output (64.64):");
console2.logInt(qj64);
return (amountIn, qj64); return (amountIn, qj64);
} }
int128 lnInner = _ln(inner); int128 lnInner = _ln(inner);
console2.log("ln(inner):");
console2.logInt(lnInner);
int128 b_lnInner = b.mul(lnInner); int128 b_lnInner = b.mul(lnInner);
console2.log("b*ln(inner):");
console2.logInt(b_lnInner);
amountOut = b_lnInner; amountOut = b_lnInner;
console2.log("amountOut = b*ln(inner):");
console2.logInt(amountOut);
console2.log("amountOut (final 64.64 amount):");
console2.logInt(amountOut);
// Safety check // Safety check
if (amountOut <= 0) { if (amountOut <= 0) {
console2.log("WARNING: x64 <= 0, returning 0");
return (0, 0); return (0, 0);
} }
} }
@@ -265,60 +202,37 @@ library LMSRStabilized {
// Compute r0 = exp((q_i - q_j) / b) directly using invB // 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(s.qInternal[i].sub(s.qInternal[j]).mul(invB));
console2.log("\n=== Max Input/Output Calculation ===");
console2.log("Limit price (64x64):");
console2.logInt(limitPrice);
console2.log("Current price ratio r0 (e_i/e_j, 64x64):");
console2.logInt(r0);
// Mirror swapAmountsForExactInput behavior: treat invalid r0 as an error condition. // Mirror swapAmountsForExactInput behavior: treat invalid r0 as an error condition.
// Revert if r0 is non-positive (no finite trade under a price limit). // Revert if r0 is non-positive (no finite trade under a price limit).
require(r0 > int128(0), "LMSR: r0<=0"); require(r0 > int128(0), "LMSR: r0<=0");
// If current price already exceeds or equals limit, revert the same way swapAmountsForExactInput does. // If current price already exceeds or equals limit, revert the same way swapAmountsForExactInput does.
if (r0 >= limitPrice) { if (r0 >= limitPrice) {
console2.log("Limit price is <= current price: reverting");
revert("LMSR: limitPrice <= current price"); revert("LMSR: limitPrice <= current price");
} }
// Calculate the price change factor: limitPrice/r0 // Calculate the price change factor: limitPrice/r0
int128 priceChangeFactor = limitPrice.div(r0); int128 priceChangeFactor = limitPrice.div(r0);
console2.log("Price change factor (limitPrice/r0):");
console2.logInt(priceChangeFactor);
// ln(priceChangeFactor) gives us the maximum allowed delta in the exponent // ln(priceChangeFactor) gives us the maximum allowed delta in the exponent
int128 maxDeltaExponent = _ln(priceChangeFactor); int128 maxDeltaExponent = _ln(priceChangeFactor);
console2.log("Max delta exponent ln(priceChangeFactor):");
console2.logInt(maxDeltaExponent);
// Maximum input capable of reaching the price limit: // Maximum input capable of reaching the price limit:
// x_max = b * ln(limitPrice / r0) // x_max = b * ln(limitPrice / r0)
int128 amountInMax = b.mul(maxDeltaExponent); int128 amountInMax = b.mul(maxDeltaExponent);
console2.log("Max input to reach limit (64.64):");
console2.logInt(amountInMax);
// The maximum output y corresponding to that input: // The maximum output y corresponding to that input:
// y = b * ln(1 + (e_i/e_j) * (1 - exp(-x_max/b))) // y = b * ln(1 + (e_i/e_j) * (1 - exp(-x_max/b)))
int128 expTerm = ONE.sub(_exp(maxDeltaExponent.neg())); int128 expTerm = ONE.sub(_exp(maxDeltaExponent.neg()));
console2.log("1 - exp(-maxDeltaExponent):");
console2.logInt(expTerm);
int128 innerTerm = r0.mul(expTerm); int128 innerTerm = r0.mul(expTerm);
console2.log("e_i/e_j * expTerm:");
console2.logInt(innerTerm);
int128 lnTerm = _ln(ONE.add(innerTerm)); int128 lnTerm = _ln(ONE.add(innerTerm));
console2.log("ln(1 + innerTerm):");
console2.logInt(lnTerm);
int128 maxOutput = b.mul(lnTerm); int128 maxOutput = b.mul(lnTerm);
console2.log("Max output (b * lnTerm):");
console2.logInt(maxOutput);
// Current balance of asset j (in 64.64) // Current balance of asset j (in 64.64)
int128 qj64 = s.qInternal[j]; int128 qj64 = s.qInternal[j];
console2.log("Current j balance (64.64):");
console2.logInt(qj64);
// Initialize outputs to the computed maxima // Initialize outputs to the computed maxima
amountIn = amountInMax; amountIn = amountInMax;
@@ -326,7 +240,6 @@ library LMSRStabilized {
// If the calculated maximum output exceeds the balance, cap output and solve for input. // If the calculated maximum output exceeds the balance, cap output and solve for input.
if (maxOutput > qj64) { if (maxOutput > qj64) {
console2.log("Max output exceeds balance, capping to balance");
amountOut = qj64; amountOut = qj64;
// Solve inverse relation for input given capped output: // Solve inverse relation for input given capped output:
@@ -336,19 +249,12 @@ library LMSRStabilized {
// a = -b * ln( (r0 + 1 - E) / r0 ) = b * ln( r0 / (r0 + 1 - E) ) // a = -b * ln( (r0 + 1 - E) / r0 ) = b * ln( r0 / (r0 + 1 - E) )
int128 E = _exp(amountOut.mul(invB)); // exp(y/b) int128 E = _exp(amountOut.mul(invB)); // exp(y/b)
int128 rhs = r0.add(ONE).sub(E); // r0 + 1 - E int128 rhs = r0.add(ONE).sub(E); // r0 + 1 - E
console2.log("E = exp(y/b):");
console2.logInt(E);
console2.log("rhs = r0 + 1 - E:");
console2.logInt(rhs);
// If rhs <= 0 due to numerical issues, fall back to amountInMax // If rhs <= 0 due to numerical issues, fall back to amountInMax
if (rhs <= int128(0)) { if (rhs <= int128(0)) {
console2.log("Numerical issue solving inverse; using amountInMax as fallback");
amountIn = amountInMax; amountIn = amountInMax;
} else { } else {
amountIn = b.mul(_ln(r0.div(rhs))); amountIn = b.mul(_ln(r0.div(rhs)));
console2.log("Computed input required for capped output (64.64):");
console2.logInt(amountIn);
} }
} }
@@ -674,19 +580,9 @@ library LMSRStabilized {
require(amountIn > int128(0), "LMSR: amountIn <= 0"); require(amountIn > int128(0), "LMSR: amountIn <= 0");
require(amountOut > int128(0), "LMSR: amountOut <= 0"); require(amountOut > int128(0), "LMSR: amountOut <= 0");
console2.log("\n=== Applying Swap ===");
console2.log("Input asset:", i);
console2.log("Output asset:", j);
console2.log("Amount in (64.64):");
console2.logInt(amountIn);
console2.log("Amount out (64.64):");
console2.logInt(amountOut);
// Update internal balances // Update internal balances
s.qInternal[i] = s.qInternal[i].add(amountIn); s.qInternal[i] = s.qInternal[i].add(amountIn);
s.qInternal[j] = s.qInternal[j].sub(amountOut); s.qInternal[j] = s.qInternal[j].sub(amountOut);
console2.log("=== Swap Applied (qInternal updated) ===\n");
} }
@@ -697,27 +593,16 @@ library LMSRStabilized {
function updateForProportionalChange(State storage s, int128[] memory newQInternal) internal { function updateForProportionalChange(State storage s, int128[] memory newQInternal) internal {
require(newQInternal.length == s.nAssets, "LMSR: length mismatch"); require(newQInternal.length == s.nAssets, "LMSR: length mismatch");
console2.log("LMSR.updateForProportionalChange: start");
// Compute new total for validation // Compute new total for validation
int128 newTotal = _computeSizeMetric(newQInternal); int128 newTotal = _computeSizeMetric(newQInternal);
console2.log("new total");
console2.logInt(newTotal);
require(newTotal > int128(0), "LMSR: new total zero"); require(newTotal > int128(0), "LMSR: new total zero");
// With kappa formulation, b automatically scales with pool size
int128 newB = s.kappa.mul(newTotal);
console2.log("new effective b");
console2.logInt(newB);
// Update the cached qInternal with new values // Update the cached qInternal with new values
for (uint i = 0; i < s.nAssets; ) { for (uint i = 0; i < s.nAssets; ) {
s.qInternal[i] = newQInternal[i]; s.qInternal[i] = newQInternal[i];
unchecked { i++; } unchecked { i++; }
} }
console2.log("LMSR.updateForProportionalChange: end");
} }
/// @notice Price-share of asset i: exp(z_i) / Z (64.64) /// @notice Price-share of asset i: exp(z_i) / Z (64.64)
@@ -904,8 +789,6 @@ library LMSRStabilized {
/// @notice De-initialize the LMSR state when the entire pool is drained. /// @notice De-initialize the LMSR state when the entire pool is drained.
/// This resets the state so the pool can be re-initialized by init(...) on next mint. /// This resets the state so the pool can be re-initialized by init(...) on next mint.
function deinit(State storage s) internal { function deinit(State storage s) internal {
console2.log("LMSR.deinit: resetting state");
// Reset core state // Reset core state
s.nAssets = 0; s.nAssets = 0;
s.kappa = int128(0); s.kappa = int128(0);

View File

@@ -1,7 +1,6 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30; pragma solidity ^0.8.30;
import "forge-std/console2.sol";
import "@abdk/ABDKMath64x64.sol"; import "@abdk/ABDKMath64x64.sol";
import "./LMSRStabilized.sol"; import "./LMSRStabilized.sol";
@@ -40,7 +39,6 @@ library LMSRStabilizedBalancedPair {
// If not exactly a two-asset pool, fall back to the general routine. // If not exactly a two-asset pool, fall back to the general routine.
if (s.nAssets != 2) { if (s.nAssets != 2) {
console2.log('balanced2: fallback nAssets!=n2');
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice); return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
} }
@@ -48,7 +46,6 @@ library LMSRStabilizedBalancedPair {
int128 b = LMSRStabilized._computeB(s); int128 b = LMSRStabilized._computeB(s);
// Guard: if b not positive, fallback to exact implementation (will revert there if necessary) // Guard: if b not positive, fallback to exact implementation (will revert there if necessary)
if (!(b > int128(0))) { if (!(b > int128(0))) {
console2.log("balanced2: fallback b<=0");
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice); return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
} }
int128 invB = ABDKMath64x64.div(ONE, b); int128 invB = ABDKMath64x64.div(ONE, b);
@@ -58,8 +55,6 @@ library LMSRStabilizedBalancedPair {
// If a positive limitPrice is given, attempt a 2-asset near-parity polynomial solution // If a positive limitPrice is given, attempt a 2-asset near-parity polynomial solution
if (limitPrice > int128(0)) { if (limitPrice > int128(0)) {
console2.log("balanced2: handling limitPrice via small-delta approx");
// Approximate r0 = exp(delta) using Taylor: 1 + δ + δ^2/2 + δ^3/6 // Approximate r0 = exp(delta) using Taylor: 1 + δ + δ^2/2 + δ^3/6
int128 delta_sq = delta.mul(delta); int128 delta_sq = delta.mul(delta);
int128 delta_cu = delta_sq.mul(delta); int128 delta_cu = delta_sq.mul(delta);
@@ -68,19 +63,13 @@ library LMSRStabilizedBalancedPair {
.add(delta_sq.div(ABDKMath64x64.fromUInt(2))) .add(delta_sq.div(ABDKMath64x64.fromUInt(2)))
.add(delta_cu.div(ABDKMath64x64.fromUInt(6))); .add(delta_cu.div(ABDKMath64x64.fromUInt(6)));
console2.log("r0_approx:");
console2.logInt(r0_approx);
// If limitPrice <= r0 (current price) we must revert (same semantic as original) // If limitPrice <= r0 (current price) we must revert (same semantic as original)
if (limitPrice <= r0_approx) { if (limitPrice <= r0_approx) {
console2.log("balanced2: limitPrice <= r0_approx -> revert");
revert("LMSR: limitPrice <= current price"); revert("LMSR: limitPrice <= current price");
} }
// Ratio = limitPrice / r0_approx // Ratio = limitPrice / r0_approx
int128 ratio = limitPrice.div(r0_approx); int128 ratio = limitPrice.div(r0_approx);
console2.log("limitPrice/r0_approx:");
console2.logInt(ratio);
// x = ratio - 1; use Taylor for ln(1+x) when |x| is small // x = ratio - 1; use Taylor for ln(1+x) when |x| is small
int128 x = ratio.sub(ONE); int128 x = ratio.sub(ONE);
@@ -90,7 +79,6 @@ library LMSRStabilizedBalancedPair {
int128 X_MAX = ABDKMath64x64.divu(1, 10); // 0.1 int128 X_MAX = ABDKMath64x64.divu(1, 10); // 0.1
if (absX > X_MAX) { if (absX > X_MAX) {
// Too large to safely approximate; fall back to exact computation // Too large to safely approximate; fall back to exact computation
console2.log("balanced2: fallback limitPrice ratio too far from 1");
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice); return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
} }
@@ -101,63 +89,34 @@ library LMSRStabilizedBalancedPair {
.sub(x_sq.div(ABDKMath64x64.fromUInt(2))) .sub(x_sq.div(ABDKMath64x64.fromUInt(2)))
.add(x_cu.div(ABDKMath64x64.fromUInt(3))); .add(x_cu.div(ABDKMath64x64.fromUInt(3)));
console2.log("lnRatioApprox (64x64):");
console2.logInt(lnRatioApprox);
// aLimitOverB = ln(limitPrice / r0) approximated // aLimitOverB = ln(limitPrice / r0) approximated
int128 aLimitOverB = lnRatioApprox; int128 aLimitOverB = lnRatioApprox;
// Must be > 0; otherwise fall back // Must be > 0; otherwise fall back
if (!(aLimitOverB > int128(0))) { if (!(aLimitOverB > int128(0))) {
console2.log("balanced2: fallback non-positive aLimitOverB");
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice); return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
} }
// aLimit = b * aLimitOverB (in Q64.64) // aLimit = b * aLimitOverB (in Q64.64)
int128 aLimit64 = b.mul(aLimitOverB); int128 aLimit64 = b.mul(aLimitOverB);
console2.log("aLimit64 (64x64):");
console2.logInt(aLimit64);
// If computed aLimit is less than requested a, use the truncated value. // If computed aLimit is less than requested a, use the truncated value.
if (aLimit64 < a) { if (aLimit64 < a) {
console2.log("balanced2: truncating input a to aLimit64 due to limitPrice");
console2.log("original a:");
console2.logInt(a);
console2.log("truncated aLimit64:");
console2.logInt(aLimit64);
a = aLimit64; a = aLimit64;
} else { } else {
console2.log("balanced2: limitPrice does not truncate input"); // console2.log("balanced2: limitPrice does not truncate input");
} }
// Note: after potential truncation we continue with the polynomial approximation below // Note: after potential truncation we continue with the polynomial approximation below
} }
// Debug: entry trace
console2.log("balanced2: enter");
console2.log("i", i);
console2.log("j", j);
console2.log("nAssets", s.nAssets);
console2.log("a (64x64):");
console2.logInt(a);
console2.log("b (64x64):");
console2.logInt(b);
console2.log("invB (64x64):");
console2.logInt(invB);
// Small-signal delta already computed above; reuse it // Small-signal delta already computed above; reuse it
int128 absDelta = delta >= int128(0) ? delta : delta.neg(); int128 absDelta = delta >= int128(0) ? delta : delta.neg();
console2.log("delta (q_i - q_j)/b:");
console2.logInt(delta);
console2.log("absDelta:");
console2.logInt(absDelta);
// Allow balanced pools only: require |delta| <= 1% (approx ln(1.01) ~ 0.00995; we use conservative 0.01) // Allow balanced pools only: require |delta| <= 1% (approx ln(1.01) ~ 0.00995; we use conservative 0.01)
int128 DELTA_MAX = ABDKMath64x64.divu(1, 100); // 0.01 int128 DELTA_MAX = ABDKMath64x64.divu(1, 100); // 0.01
if (absDelta > DELTA_MAX) { if (absDelta > DELTA_MAX) {
// Not balanced within 1% -> use exact routine // Not balanced within 1% -> use exact routine
console2.log("balanced2: fallback delta too large");
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice); return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
} }
@@ -165,18 +124,13 @@ library LMSRStabilizedBalancedPair {
int128 u = a.mul(invB); int128 u = a.mul(invB);
if (u <= int128(0)) { if (u <= int128(0)) {
// Non-positive input -> behave like exact implementation (will revert if invalid) // Non-positive input -> behave like exact implementation (will revert if invalid)
console2.log("balanced2: fallback u<=0");
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice); return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
} }
console2.log("u = a/b (64x64):");
console2.logInt(u);
// Restrict to a conservative polynomial radius for accuracy; fallback otherwise. // Restrict to a conservative polynomial radius for accuracy; fallback otherwise.
// We choose u <= 0.5 (0.5 in Q64.64) as safe for cubic approximation in typical parameters. // We choose u <= 0.5 (0.5 in Q64.64) as safe for cubic approximation in typical parameters.
int128 U_MAX = ABDKMath64x64.divu(1, 2); // 0.5 int128 U_MAX = ABDKMath64x64.divu(1, 2); // 0.5
if (u > U_MAX) { if (u > U_MAX) {
console2.log("balanced2: fallback u too large");
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice); return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
} }
@@ -200,39 +154,26 @@ library LMSRStabilizedBalancedPair {
if (u <= U_TIER1) { if (u <= U_TIER1) {
// Cheap quadratic ln(1+X) ≈ X - X^2/2 // Cheap quadratic ln(1+X) ≈ X - X^2/2
lnApprox = X.sub(X2.div(ABDKMath64x64.fromUInt(2))); lnApprox = X.sub(X2.div(ABDKMath64x64.fromUInt(2)));
console2.log("balanced2: using tier1 quadratic approx");
} else if (u <= U_MAX_LOCAL) { } else if (u <= U_MAX_LOCAL) {
// Secondary cubic correction: ln(1+X) ≈ X - X^2/2 + X^3/3 // Secondary cubic correction: ln(1+X) ≈ X - X^2/2 + X^3/3
int128 X3 = X2.mul(X); int128 X3 = X2.mul(X);
lnApprox = X.sub(X2.div(ABDKMath64x64.fromUInt(2))).add(X3.div(ABDKMath64x64.fromUInt(3))); lnApprox = X.sub(X2.div(ABDKMath64x64.fromUInt(2))).add(X3.div(ABDKMath64x64.fromUInt(3)));
console2.log("balanced2: using tier2 cubic approx");
} else { } else {
// u beyond allowed range - fallback // u beyond allowed range - fallback
console2.log("balanced2: fallback u too large for approximation");
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice); return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
} }
console2.log("lnApprox (64x64):");
console2.logInt(lnApprox);
int128 approxOut = b.mul(lnApprox); int128 approxOut = b.mul(lnApprox);
console2.log("approxOut (64x64):");
console2.logInt(approxOut);
// Safety sanity: approximation must be > 0 // Safety sanity: approximation must be > 0
if (approxOut <= int128(0)) { if (approxOut <= int128(0)) {
console2.log("balanced2: fallback approxOut <= 0");
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice); return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
} }
// Cap to available j balance: if approximated output exceeds q_j, it's likely approximation break; // Cap to available j balance: if approximated output exceeds q_j, it's likely approximation break;
// fall back to the exact solver to handle capping/edge cases. // fall back to the exact solver to handle capping/edge cases.
int128 qj64 = s.qInternal[j]; int128 qj64 = s.qInternal[j];
console2.log("qj64 (64x64):");
console2.logInt(qj64);
if (approxOut >= qj64) { if (approxOut >= qj64) {
console2.log("balanced2: fallback approxOut >= qj");
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice); return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
} }
@@ -240,15 +181,8 @@ library LMSRStabilizedBalancedPair {
amountIn = a; amountIn = a;
amountOut = approxOut; amountOut = approxOut;
console2.log("balanced2: returning approx results");
console2.log("amountIn (64x64):");
console2.logInt(amountIn);
console2.log("amountOut (64x64):");
console2.logInt(amountOut);
// Final guard: ensure output is sensible and not NaN-like (rely on positivity checks above) // Final guard: ensure output is sensible and not NaN-like (rely on positivity checks above)
if (amountOut < int128(0)) { if (amountOut < int128(0)) {
console2.log("balanced2: fallback final guard amountOut<0");
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice); return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
} }

View File

@@ -2,8 +2,9 @@
pragma solidity ^0.8.30; pragma solidity ^0.8.30;
import "./IPartyPlanner.sol"; import "./IPartyPlanner.sol";
import "./PartyPool.sol";
import "./LMSRStabilized.sol"; import "./LMSRStabilized.sol";
import "./PartyPool.sol";
import "./PartyPoolBalancedPair.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
@@ -48,16 +49,9 @@ contract PartyPlanner is IPartyPlanner {
require(_kappa > int128(0), "Planner: kappa must be > 0"); require(_kappa > int128(0), "Planner: kappa must be > 0");
// Create a new PartyPool instance (kappa-based constructor) // Create a new PartyPool instance (kappa-based constructor)
pool = new PartyPool( pool = _stable && _tokens.length == 2 ?
name_, new PartyPoolBalancedPair(name_, symbol_, _tokens, _bases, _kappa, _swapFeePpm, _flashFeePpm) :
symbol_, new PartyPool(name_, symbol_, _tokens, _bases, _kappa, _swapFeePpm, _flashFeePpm);
_tokens,
_bases,
_kappa,
_swapFeePpm,
_flashFeePpm,
_stable
);
_allPools.push(pool); _allPools.push(pool);
_poolSupported[pool] = true; _poolSupported[pool] = true;

View File

@@ -1,16 +1,12 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30; pragma solidity ^0.8.30;
import "forge-std/console2.sol";
import "@abdk/ABDKMath64x64.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "./LMSRStabilized.sol"; import "./PoolLib.sol";
import "./LMSRStabilizedBalancedPair.sol";
import "./IPartyPool.sol"; import "./IPartyPool.sol";
import "./IPartyFlashCallback.sol"; import {PoolBase} from "./PoolBase.sol";
/// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token /// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token
/// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model. /// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model.
@@ -21,37 +17,13 @@ import "./IPartyFlashCallback.sol";
/// - Exact-input swaps and swaps-to-price-limits, /// - Exact-input swaps and swaps-to-price-limits,
/// - Flash loans via a callback interface. /// - Flash loans via a callback interface.
/// ///
/// @dev The contract stores per-token uint "bases" used to scale token units into the internal Q64.64 /// @dev The contract uses PoolLib for all implementation logic and maintains state in a PoolLib.State struct
/// representation used by the LMSR library. Cached on-chain uint balances are kept to reduce balanceOf calls. contract PartyPool is PoolBase, IPartyPool {
/// The contract uses ceiling/floor rules described in function comments to bias rounding in favor of the pool using PoolLib for PoolLib.State;
/// (i.e., floor outputs to users, ceil inputs/fees where appropriate).
contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
using ABDKMath64x64 for int128;
using LMSRStabilized for LMSRStabilized.State;
using SafeERC20 for IERC20;
//
// Immutable pool configuration
//
/// @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
/// @inheritdoc IPartyPool
function numTokens() external view returns (uint256) { return tokens.length; }
/// @inheritdoc IPartyPool
function allTokens() external view returns (IERC20[] memory) { return tokens; }
// NOTE that the slippage target is only exactly achieved in completely balanced pools where all assets are
// priced the same. This target is actually a minimum slippage that the pool imposes on traders, and the actual
// slippage cost can be multiples bigger in practice due to pool inventory imbalances.
/// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q) /// @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 /// @dev Pool is constructed with a fixed κ. Clients may use LMSRStabilized.computeKappaFromSlippage(...) to
/// should use LMSRStabilized.computeKappaFromSlippage(...) to derive κ and pass it here. /// derive κ and pass it here.
int128 public immutable kappa; // kappa in Q64.64 int128 public immutable kappa; // kappa in Q64.64
/// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations. /// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations.
@@ -60,29 +32,24 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts. /// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
uint256 public immutable flashFeePpm; uint256 public immutable flashFeePpm;
//
// Internal state
//
LMSRStabilized.State internal lmsr;
/// @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
// 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 /// @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. /// @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. /// @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). /// @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). /// @dev LP tokens are minted in units equal to ABDK.mulu(lastTotalQ64x64, LP_SCALE).
@@ -90,77 +57,37 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
/// @param name_ LP token name /// @param name_ LP token name
/// @param symbol_ LP token symbol /// @param symbol_ LP token symbol
/// @param _tokens token addresses (n) /// @param tokens_ token addresses (n)
/// @param _bases scaling bases for each token (n) - used when converting to/from internal 64.64 amounts /// @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 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 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 flashFeePpm_ fee in parts-per-million, taken for flash loans
/// @param _stable if true and assets.length==2, then the optimization for 2-asset stablecoin pools is activated.
constructor( constructor(
string memory name_, string memory name_,
string memory symbol_, string memory symbol_,
IERC20[] memory _tokens, IERC20[] memory tokens_,
uint256[] memory _bases, uint256[] memory bases_,
int128 _kappa, int128 kappa_,
uint256 _swapFeePpm, uint256 swapFeePpm_,
uint256 _flashFeePpm, uint256 flashFeePpm_
bool _stable
) ERC20(name_, symbol_) { ) ERC20(name_, symbol_) {
require(_tokens.length > 1, "Pool: need >1 asset"); kappa = kappa_;
require(_tokens.length == _bases.length, "Pool: lengths mismatch"); require(swapFeePpm_ < 1_000_000, "Pool: fee >= ppm");
tokens = _tokens; swapFeePpm = swapFeePpm_;
bases = _bases; require(flashFeePpm_ < 1_000_000, "Pool: flash fee >= ppm");
kappa = _kappa; flashFeePpm = flashFeePpm_;
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;
uint256 n = _tokens.length; // Initialize state using library
s.initialize(tokens_, bases_);
// 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);
} }
/* ---------------------- /* ----------------------
Initialization / Mint / Burn (LP token managed) Initialization / Mint / Burn (LP token managed)
---------------------- */ ---------------------- */
/// @inheritdoc IPartyPool /// @inheritdoc IPartyPool
function mintDepositAmounts(uint256 lpTokenAmount) public view returns (uint256[] memory depositAmounts) { function mintDepositAmounts(uint256 lpTokenAmount) public view returns (uint256[] memory depositAmounts) {
uint256 n = tokens.length; return s.mintDepositAmounts(lpTokenAmount, totalSupply());
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. /// @notice Initial mint to set up pool for the first time.
@@ -170,37 +97,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 /// @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 function initialMint(address receiver, uint256 lpTokens) external nonReentrant
returns (uint256 lpMinted) { returns (uint256 lpMinted) {
uint256 n = tokens.length; lpMinted = s.initialMint(receiver, lpTokens, kappa, totalSupply());
// 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); _mint(receiver, lpMinted);
emit Mint(address(0), receiver, depositAmounts, lpMinted);
} }
/// @notice Proportional mint for existing pool. /// @notice Proportional mint for existing pool.
@@ -213,94 +111,13 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore. /// @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 function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external nonReentrant
returns (uint256 lpMinted) { returns (uint256 lpMinted) {
require(deadline == 0 || block.timestamp <= deadline, "mint: deadline exceeded"); lpMinted = s.mint(payer, receiver, lpTokenAmount, deadline, totalSupply());
uint256 n = tokens.length; _mint(receiver, lpMinted);
// 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;
} }
/// @inheritdoc IPartyPool /// @inheritdoc IPartyPool
function burnReceiveAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts) { function burnReceiveAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts) {
return _burnReceiveAmounts(lpTokenAmount); return s.burnReceiveAmounts(lpTokenAmount, totalSupply());
}
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;
} }
/// @notice Burn LP tokens and withdraw the proportional basket to receiver. /// @notice Burn LP tokens and withdraw the proportional basket to receiver.
@@ -311,67 +128,15 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
/// @param lpAmount amount of LP tokens to burn (proportional withdrawal) /// @param lpAmount amount of LP tokens to burn (proportional withdrawal)
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore. /// @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 { function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external nonReentrant {
require(deadline == 0 || block.timestamp <= deadline, "burn: deadline exceeded"); /* uint256[] memory withdrawAmounts = */ s.burn(payer, receiver, lpAmount, deadline, totalSupply(), balanceOf(payer));
uint256 n = tokens.length;
require(lpAmount > 0, "burn: zero lp");
uint256 supply = totalSupply(); // Handle LP token burning with allowance
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) { if (msg.sender != payer) {
uint256 allowed = allowance(payer, msg.sender); uint256 allowed = allowance(payer, msg.sender);
require(allowed >= lpAmount, "burn: allowance insufficient"); require(allowed >= lpAmount, "burn: allowance insufficient");
_approve(payer, msg.sender, allowed - lpAmount); _approve(payer, msg.sender, allowed - lpAmount);
} }
_burn(payer, lpAmount); _burn(payer, lpAmount);
emit Burn(payer, receiver, withdrawAmounts, lpAmount);
} }
/* ---------------------- /* ----------------------
@@ -384,9 +149,8 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
uint256 outputTokenIndex, uint256 outputTokenIndex,
uint256 maxAmountIn, uint256 maxAmountIn,
int128 limitPrice int128 limitPrice
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) { ) virtual external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice); return s.swapAmounts(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm);
return (grossIn, outUint, feeUint);
} }
/// @inheritdoc IPartyPool /// @inheritdoc IPartyPool
@@ -395,22 +159,18 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
uint256 outputTokenIndex, uint256 outputTokenIndex,
int128 limitPrice int128 limitPrice
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) { ) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapToLimit(inputTokenIndex, outputTokenIndex, limitPrice); return s.swapToLimitAmounts(inputTokenIndex, outputTokenIndex, limitPrice, swapFeePpm);
return (grossIn, outUint, feeUint);
} }
/// @inheritdoc IPartyPool
function swapMintAmounts(
uint256 inputTokenIndex,
uint256 maxAmountIn
) external view returns (uint256 totalTransfer, uint256 amountIn, uint256 fee, uint256 lpMinted) {
return s.swapMintAmounts(inputTokenIndex, maxAmountIn, swapFeePpm, totalSupply());
}
/// @notice Swap input token i -> token j. Payer must approve token i. /// @inheritdoc IPartyPool
/// @dev This function transfers the exact gross input (including fee) from payer and sends the computed output to receiver.
/// Non-standard tokens (fee-on-transfer, rebasers) are rejected via balance checks.
/// @param payer address of the account that pays for the swap
/// @param receiver address that will receive the output tokens
/// @param inputTokenIndex index of input asset
/// @param outputTokenIndex index of output asset
/// @param maxAmountIn maximum amount of token i (uint256) to transfer in (inclusive of fees)
/// @param limitPrice maximum acceptable marginal price (64.64 fixed point). Pass 0 to ignore.
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
/// @return amountIn actual input used (uint256), amountOut actual output sent (uint256), fee fee taken from the input (uint256)
function swap( function swap(
address payer, address payer,
address receiver, address receiver,
@@ -419,40 +179,8 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
uint256 maxAmountIn, uint256 maxAmountIn,
int128 limitPrice, int128 limitPrice,
uint256 deadline uint256 deadline
) external nonReentrant returns (uint256 amountIn, uint256 amountOut, uint256 fee) { ) virtual external nonReentrant returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
uint256 n = tokens.length; return s.swap(payer, receiver, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, deadline, swapFeePpm);
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);
} }
/// @notice Swap up to the price limit; computes max input to reach limit then performs swap. /// @notice Swap up to the price limit; computes max input to reach limit then performs swap.
@@ -467,176 +195,9 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
int128 limitPrice, int128 limitPrice,
uint256 deadline uint256 deadline
) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) { ) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) {
uint256 n = tokens.length; return s.swapToLimit(payer, receiver, inputTokenIndex, outputTokenIndex, limitPrice, deadline, swapFeePpm);
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);
} }
/// @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
console2.log('stablepair optimization?', _stablePair);
(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. /// @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. /// @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. /// The function emits SwapMint (gross, net, fee) and also emits Mint for LP issuance.
@@ -653,90 +214,16 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
uint256 maxAmountIn, uint256 maxAmountIn,
uint256 deadline uint256 deadline
) external nonReentrant returns (uint256 lpMinted) { ) external nonReentrant returns (uint256 lpMinted) {
uint256 n = tokens.length; lpMinted = s.swapMint(payer, receiver, inputTokenIndex, maxAmountIn, deadline, swapFeePpm, totalSupply());
require(inputTokenIndex < n, "swapMint: idx"); _mint(receiver, lpMinted);
require(maxAmountIn > 0, "swapMint: input zero"); }
require(deadline == 0 || block.timestamp <= deadline, "swapMint: deadline");
// Ensure pool initialized /// @inheritdoc IPartyPool
require(lmsr.nAssets > 0, "swapMint: uninit pool"); function burnSwapAmounts(
uint256 lpAmount,
// compute fee on gross maxAmountIn to get an initial net estimate (we'll recompute based on actual used) uint256 inputTokenIndex
(, uint256 netUintGuess) = _computeFee(maxAmountIn); ) external view returns (uint256 amountOut) {
return s.burnSwapAmounts(lpAmount, inputTokenIndex, swapFeePpm, totalSupply());
// 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;
} }
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver. /// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver.
@@ -754,76 +241,23 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
uint256 inputTokenIndex, uint256 inputTokenIndex,
uint256 deadline uint256 deadline
) external nonReentrant returns (uint256 amountOutUint) { ) external nonReentrant returns (uint256 amountOutUint) {
uint256 n = tokens.length; amountOutUint = s.burnSwap(payer, receiver, lpAmount, inputTokenIndex, deadline, swapFeePpm, totalSupply(), balanceOf(payer));
require(inputTokenIndex < n, "burnSwap: idx");
require(lpAmount > 0, "burnSwap: zero lp");
require(deadline == 0 || block.timestamp <= deadline, "burnSwap: deadline");
uint256 supply = totalSupply(); // Handle LP token burning with allowance
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)
if (msg.sender != payer) { if (msg.sender != payer) {
uint256 allowed = allowance(payer, msg.sender); uint256 allowed = allowance(payer, msg.sender);
require(allowed >= lpAmount, "burnSwap: allowance insufficient"); require(allowed >= lpAmount, "burnSwap: allowance insufficient");
_approve(payer, msg.sender, allowed - lpAmount); _approve(payer, msg.sender, allowed - lpAmount);
} }
_burn(payer, 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 /// @inheritdoc IPartyPool
function flashRepaymentAmounts(uint256[] memory loanAmounts) external view function flashRepaymentAmounts(uint256[] memory loanAmounts) external view
returns (uint256[] memory repaymentAmounts) { returns (uint256[] memory repaymentAmounts) {
repaymentAmounts = new uint256[](tokens.length); return s.flashRepaymentAmounts(loanAmounts, flashFeePpm);
for (uint256 i = 0; i < tokens.length; i++) {
uint256 amount = loanAmounts[i];
if (amount > 0) {
repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm);
}
}
} }
/// @notice Receive token amounts and require them to be repaid plus a fee inside a callback. /// @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). /// @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 /// This function verifies that, after the callback returns, the pool's balances have increased by at least the fees
@@ -836,137 +270,19 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
uint256[] memory amounts, uint256[] memory amounts,
bytes calldata data bytes calldata data
) external nonReentrant { ) external nonReentrant {
require(recipient != address(0), "flash: zero recipient"); s.flash(recipient, amounts, data, flashFeePpm);
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;
} }
/// @notice Marginal price of `base` in terms of `quote` (p_quote / p_base) as Q64.64 /// @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. /// @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) { function price(uint256 baseTokenIndex, uint256 quoteTokenIndex) external view returns (int128) {
uint256 n = tokens.length; return s.price(baseTokenIndex, quoteTokenIndex);
require(baseTokenIndex < n && quoteTokenIndex < n, "price: idx");
require(lmsr.nAssets > 0, "price: uninit");
return lmsr.price(baseTokenIndex, quoteTokenIndex);
} }
/// @notice Price of one LP token denominated in `quote` asset as Q64.64 /// @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 /// @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). /// to return price per LP token unit in quote asset (raw 64.64).
function poolPrice(uint256 quoteTokenIndex) external view returns (int128) { function poolPrice(uint256 quoteTokenIndex) external view returns (int128) {
uint256 n = tokens.length; return s.poolPrice(quoteTokenIndex, totalSupply());
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);
} }
/// @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;
}
} }

View File

@@ -0,0 +1,42 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import "./PartyPool.sol";
import {PoolLibBalancedPair} from "./PoolLibBalancedPair.sol";
contract PartyPoolBalancedPair is PartyPool {
constructor(
string memory name_,
string memory symbol_,
IERC20[] memory tokens_,
uint256[] memory bases_,
int128 kappa_,
uint256 swapFeePpm_,
uint256 flashFeePpm_
) PartyPool(name_, symbol_, tokens_, bases_, kappa_, swapFeePpm_, flashFeePpm_) {
}
function swapAmounts(
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice
) virtual override external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
return PoolLibBalancedPair.swapAmounts(
s, inputTokenIndex, outputTokenIndex,
maxAmountIn, limitPrice, swapFeePpm);
}
function swap(
address payer,
address receiver,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice,
uint256 deadline
) virtual override external nonReentrant returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
return PoolLibBalancedPair.swap(s, payer, receiver, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, deadline, swapFeePpm);
}
}

14
src/PoolBase.sol Normal file
View File

@@ -0,0 +1,14 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "./PoolLib.sol";
/// @notice This contract has all the storage for PartyPool and can be inherited by implementation contracts that
/// are delegate-called
abstract contract PoolBase is ERC20, ReentrancyGuard {
/// @notice Pool state containing all storage variables
PoolLib.State internal s;
}

935
src/PoolLib.sol Normal file
View File

@@ -0,0 +1,935 @@
// 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(
PoolLib.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(
PoolLib.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(
PoolLib.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(
PoolLib.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(
PoolLib.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(
PoolLib.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(
PoolLib.State storage state,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice,
uint256 swapFeePpm
) internal view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(
state, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm
);
return (grossIn, outUint, feeUint);
}
/// @notice Get swap amounts for swap to price limit
function swapToLimitAmounts(
PoolLib.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 Get amounts for swapMint operation
function swapMintAmounts(
PoolLib.State storage state,
uint256 inputTokenIndex,
uint256 maxAmountIn,
uint256 swapFeePpm,
uint256 totalSupply
) internal view returns (uint256 totalTransfer, uint256 amountIn, uint256 fee, uint256 lpMinted) {
uint256 n = state.tokens.length;
require(inputTokenIndex < n, "swapMintAmounts: idx");
require(maxAmountIn > 0, "swapMintAmounts: input zero");
require(state.lmsr.nAssets > 0, "swapMintAmounts: 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), "swapMintAmounts: 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, "swapMintAmounts: input zero after internal conversion");
// Compute fee on actual used input and total transfer amount
uint256 feeUintActual = _ceilFee(amountInUint, swapFeePpm);
uint256 totalTransferAmount = amountInUint + feeUintActual;
require(totalTransferAmount > 0 && totalTransferAmount <= maxAmountIn, "swapMintAmounts: transfer exceeds max");
// Compute old and new scaled size metrics
int128 oldTotal = _computeSizeMetric(state.lmsr.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);
uint256 actualLpToMint;
if (totalSupply == 0) {
actualLpToMint = newScaled;
} else {
require(oldScaled > 0, "swapMintAmounts: oldScaled zero");
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
if (delta > 0) {
actualLpToMint = (totalSupply * delta) / oldScaled;
} else {
actualLpToMint = 0;
}
}
require(actualLpToMint > 0, "swapMintAmounts: zero LP minted");
return (totalTransferAmount, amountInUint, feeUintActual, actualLpToMint);
}
/// @notice Execute exact input swap
function swap(
PoolLib.State storage state,
address payer,
address receiver,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice,
uint256 deadline,
uint256 swapFeePpm
) 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);
// 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(
PoolLib.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 Get amounts for burnSwap operation
function burnSwapAmounts(
PoolLib.State storage state,
uint256 lpAmount,
uint256 inputTokenIndex,
uint256 swapFeePpm,
uint256 totalSupply
) internal view returns (uint256 amountOut) {
uint256 n = state.tokens.length;
require(inputTokenIndex < n, "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);
// Use LMSR view to compute single-asset payout
(int128 payoutInternal, ) = state.lmsr.swapAmountsForBurn(inputTokenIndex, alpha);
// Convert payoutInternal -> uint (floor) to favor pool
uint256 amountOutUint = _internalToUintFloor(payoutInternal, state.bases[inputTokenIndex]);
require(amountOutUint > 0, "burnSwapAmounts: output zero");
// Apply swap fee to the output
if (swapFeePpm > 0) {
uint256 feeUint = _ceilFee(amountOutUint, swapFeePpm);
require(amountOutUint > feeUint, "burnSwapAmounts: fee exceeds output");
amountOutUint -= feeUint;
}
return amountOutUint;
}
/// @notice Single-token mint (swapMint)
function swapMint(
PoolLib.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");
// Calculate amounts using view function
(uint256 totalTransfer, uint256 amountInUint, uint256 feeUintActual, uint256 actualLpToMint) = swapMintAmounts(
state, inputTokenIndex, maxAmountIn, swapFeePpm, totalSupply
);
// 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;
// Update LMSR internal state
int128 oldTotal = _computeSizeMetric(state.lmsr.qInternal);
(, int128 sizeIncreaseInternal) = state.lmsr.swapAmountsForMint(inputTokenIndex, _uintToInternalFloor(amountInUint, state.bases[inputTokenIndex]));
int128 newTotal = oldTotal.add(sizeIncreaseInternal);
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(
PoolLib.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");
// Calculate amounts using view function
amountOutUint = burnSwapAmounts(state, lpAmount, inputTokenIndex, swapFeePpm, totalSupply);
// 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(
PoolLib.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(
PoolLib.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(
PoolLib.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(
PoolLib.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(
PoolLib.State storage state,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice,
uint256 swapFeePpm
)
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) = 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 exact-input swap
function _quoteSwapExactIn_BalancedPair(
PoolLib.State storage state,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice,
uint256 swapFeePpm
)
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) = LMSRStabilizedBalancedPair.swapAmountsForExactInput(
state.lmsr, 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(
PoolLib.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;
}
}

139
src/PoolLibBalancedPair.sol Normal file
View File

@@ -0,0 +1,139 @@
// 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";
import {PoolLib} from "./PoolLib.sol";
/// @title PoolLibBalancedPair - Library with optimized functions for stablecoin pair pools
/// @dev All functions are internal and accept State as the first parameter
library PoolLibBalancedPair {
using ABDKMath64x64 for int128;
using LMSRStabilized for LMSRStabilized.State;
using PoolLib for PoolLib.State;
using SafeERC20 for IERC20;
/// @notice Get swap amounts for exact input swap
function swapAmounts(
PoolLib.State storage state,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice,
uint256 swapFeePpm
) internal view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(
state, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm
);
return (grossIn, outUint, feeUint);
}
/// @notice Internal quote for exact-input swap
function _quoteSwapExactIn(
PoolLib.State storage state,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice,
uint256 swapFeePpm
)
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) = PoolLib._computeFee(maxAmountIn, swapFeePpm);
// Convert to internal (floor)
int128 deltaInternalI = PoolLib._uintToInternalFloor(netUintForSwap, state.bases[inputTokenIndex]);
require(deltaInternalI > int128(0), "swap: input too small after fee");
// Compute internal amounts using LMSR
(amountInInternalUsed, amountOutInternal) = LMSRStabilizedBalancedPair.swapAmountsForExactInput(state.lmsr,
inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice);
// Convert actual used input internal -> uint (ceil)
amountInUintNoFee = PoolLib._internalToUintCeil(amountInInternalUsed, state.bases[inputTokenIndex]);
require(amountInUintNoFee > 0, "swap: input zero");
// Compute gross transfer including fee
feeUint = 0;
grossIn = amountInUintNoFee;
if (swapFeePpm > 0) {
feeUint = PoolLib._ceilFee(amountInUintNoFee, swapFeePpm);
grossIn += feeUint;
}
// Ensure within user max
require(grossIn <= maxAmountIn, "swap: transfer exceeds max");
// Compute output (floor)
amountOutUint = PoolLib._internalToUintFloor(amountOutInternal, state.bases[outputTokenIndex]);
require(amountOutUint > 0, "swap: output zero");
}
/// @notice Execute exact input swap
function swap(
PoolLib.State storage state,
address payer,
address receiver,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice,
uint256 deadline,
uint256 swapFeePpm
) 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);
// 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 PoolLib.Swap(payer, receiver, state.tokens[inputTokenIndex], state.tokens[outputTokenIndex], totalTransferAmount, amountOutUint);
return (totalTransferAmount, amountOutUint, feeUint);
}
}

View File

@@ -1,14 +1,15 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30; pragma solidity ^0.8.30;
import "forge-std/Test.sol"; import "../src/Deploy.sol";
import "@abdk/ABDKMath64x64.sol"; import "../src/IPartyFlashCallback.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../src/LMSRStabilized.sol"; import "../src/LMSRStabilized.sol";
import "../src/PartyPool.sol"; import "../src/PartyPool.sol";
import "@abdk/ABDKMath64x64.sol";
// Import the flash callback interface // Import the flash callback interface
import "../src/IPartyFlashCallback.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "forge-std/Test.sol";
/// @notice Test contract that implements the flash callback for testing flash loans /// @notice Test contract that implements the flash callback for testing flash loans
contract FlashBorrower is IPartyFlashCallback { contract FlashBorrower is IPartyFlashCallback {
@@ -172,7 +173,7 @@ contract GasTest is Test {
} }
// Compute kappa from slippage params and number of tokens, then construct pool with kappa // Compute kappa from slippage params and number of tokens, then construct pool with kappa
int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(ierc20Tokens.length, tradeFrac, targetSlippage); int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(ierc20Tokens.length, tradeFrac, targetSlippage);
PartyPool newPool = new PartyPool(poolName, poolName, ierc20Tokens, bases, computedKappa, feePpm, feePpm, false); PartyPool newPool = Deploy.newPartyPool(poolName, poolName, ierc20Tokens, bases, computedKappa, feePpm, feePpm, false);
// Transfer initial deposit amounts into pool before initial mint // Transfer initial deposit amounts into pool before initial mint
for (uint256 i = 0; i < numTokens; i++) { for (uint256 i = 0; i < numTokens; i++) {
@@ -212,7 +213,7 @@ contract GasTest is Test {
ierc20Tokens[i] = IERC20(tokens[i]); ierc20Tokens[i] = IERC20(tokens[i]);
} }
int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(ierc20Tokens.length, tradeFrac, targetSlippage); int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(ierc20Tokens.length, tradeFrac, targetSlippage);
PartyPool newPool = new PartyPool(poolName, poolName, ierc20Tokens, bases, computedKappa, feePpm, feePpm, true); PartyPool newPool = Deploy.newPartyPool(poolName, poolName, ierc20Tokens, bases, computedKappa, feePpm, feePpm, true);
// Transfer initial deposit amounts into pool before initial mint // Transfer initial deposit amounts into pool before initial mint
for (uint256 i = 0; i < numTokens; i++) { for (uint256 i = 0; i < numTokens; i++) {

View File

@@ -711,7 +711,7 @@ contract LMSRStabilizedTest is Test {
} }
// Path 1: Direct swap from asset 0 to asset 2 // 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 // Restore original state for second path
_updateCachedQInternal(backupQ); _updateCachedQInternal(backupQ);
@@ -724,7 +724,7 @@ contract LMSRStabilizedTest is Test {
s.qInternal[1] = s.qInternal[1].add(indirectAmountOut1); s.qInternal[1] = s.qInternal[1].add(indirectAmountOut1);
// Second swap: asset 1 -> asset 2 // 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, // The path independence property isn't perfect due to discrete swap mechanics,
// but the difference should be within reasonable bounds // but the difference should be within reasonable bounds
@@ -765,7 +765,7 @@ contract LMSRStabilizedTest is Test {
s.qInternal[1] = s.qInternal[1].add(amountOut1); s.qInternal[1] = s.qInternal[1].add(amountOut1);
// Step 2: Swap back asset 1 -> asset 0 // 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 // Calculate round-trip slippage: (initial amount - final amount) / initial amount
int128 roundTripSlippage = (amountIn1.sub(amountOut2)).div(amountIn1); int128 roundTripSlippage = (amountIn1.sub(amountOut2)).div(amountIn1);

View File

@@ -1,14 +1,15 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30; pragma solidity ^0.8.30;
import "forge-std/Test.sol"; import "../src/Deploy.sol";
import "@abdk/ABDKMath64x64.sol"; import "../src/IPartyFlashCallback.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../src/LMSRStabilized.sol"; import "../src/LMSRStabilized.sol";
import "../src/PartyPool.sol"; import "../src/PartyPool.sol";
import "@abdk/ABDKMath64x64.sol";
// Import the flash callback interface // Import the flash callback interface
import "../src/IPartyFlashCallback.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "forge-std/Test.sol";
/// @notice Test contract that implements the flash callback for testing flash loans /// @notice Test contract that implements the flash callback for testing flash loans
contract FlashBorrower is IPartyFlashCallback { contract FlashBorrower is IPartyFlashCallback {
@@ -197,7 +198,7 @@ contract PartyPoolTest is Test {
uint256 feePpm = 1000; uint256 feePpm = 1000;
int128 kappa3 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); int128 kappa3 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
pool = new PartyPool("LP", "LP", tokens, bases, kappa3, feePpm, feePpm, false); pool = Deploy.newPartyPool("LP", "LP", tokens, bases, kappa3, feePpm, feePpm, false);
// Transfer initial deposit amounts into pool before initial mint (pool expects tokens already in contract) // Transfer initial deposit amounts into pool before initial mint (pool expects tokens already in contract)
// We deposit equal amounts INIT_BAL for each token // We deposit equal amounts INIT_BAL for each token
@@ -227,7 +228,7 @@ contract PartyPoolTest is Test {
} }
int128 kappa10 = LMSRStabilized.computeKappaFromSlippage(tokens10.length, tradeFrac, targetSlippage); int128 kappa10 = LMSRStabilized.computeKappaFromSlippage(tokens10.length, tradeFrac, targetSlippage);
pool10 = new PartyPool("LP10", "LP10", tokens10, bases10, kappa10, feePpm, feePpm, false); pool10 = Deploy.newPartyPool("LP10", "LP10", tokens10, bases10, kappa10, feePpm, feePpm, false);
// Mint additional tokens for pool10 initial deposit // Mint additional tokens for pool10 initial deposit
token0.mint(address(this), INIT_BAL); token0.mint(address(this), INIT_BAL);
@@ -1231,11 +1232,11 @@ contract PartyPoolTest is Test {
// Pool with default initialization (lpTokens = 0) // Pool with default initialization (lpTokens = 0)
int128 kappaDefault = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); int128 kappaDefault = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
PartyPool poolDefault = new PartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault, feePpm, feePpm, false); PartyPool poolDefault = Deploy.newPartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault, feePpm, feePpm, false);
// Pool with custom initialization (lpTokens = custom amount) // Pool with custom initialization (lpTokens = custom amount)
int128 kappaCustom = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); int128 kappaCustom = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
PartyPool poolCustom = new PartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom, feePpm, feePpm, false); PartyPool poolCustom = Deploy.newPartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom, feePpm, feePpm, false);
// Mint additional tokens for both pools // Mint additional tokens for both pools
token0.mint(address(this), INIT_BAL * 2); token0.mint(address(this), INIT_BAL * 2);
@@ -1307,9 +1308,9 @@ contract PartyPoolTest is Test {
uint256 feePpm = 1000; uint256 feePpm = 1000;
int128 kappaDefault2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); int128 kappaDefault2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
PartyPool poolDefault = new PartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault2, feePpm, feePpm, false); PartyPool poolDefault = Deploy.newPartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault2, feePpm, feePpm, false);
int128 kappaCustom2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); int128 kappaCustom2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
PartyPool poolCustom = new PartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom2, feePpm, feePpm, false); PartyPool poolCustom = Deploy.newPartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom2, feePpm, feePpm, false);
// Mint additional tokens // Mint additional tokens
token0.mint(address(this), INIT_BAL * 4); token0.mint(address(this), INIT_BAL * 4);