From b5eab7dad124c93407e6920a3770482d3409ce10 Mon Sep 17 00:00:00 2001 From: tim Date: Tue, 23 Sep 2025 19:08:14 -0400 Subject: [PATCH] pure kappa formulation: target slippage extracted into pool creator code --- src/IPartyPlanner.sol | 38 ++++- src/IPartyPool.sol | 11 +- src/LMSRStabilized.sol | 300 ++++++++-------------------------------- src/PartyPlanner.sol | 62 +++++++-- src/PartyPool.sol | 23 ++- test/GasTest.sol | 8 +- test/PartyPlanner.t.sol | 27 ++-- test/PartyPool.t.sol | 19 ++- 8 files changed, 198 insertions(+), 290 deletions(-) diff --git a/src/IPartyPlanner.sol b/src/IPartyPlanner.sol index 68716d0..adc8872 100644 --- a/src/IPartyPlanner.sol +++ b/src/IPartyPlanner.sol @@ -10,7 +10,8 @@ interface IPartyPlanner { // Event emitted when a new pool is created event PartyStarted(PartyPool indexed pool, string name, string symbol, IERC20[] tokens); - /// @notice Creates a new PartyPool instance and initializes it with initial deposits + /// @notice Creates a new PartyPool instance and initializes it with initial deposits (legacy signature). + /// @dev Deprecated in favour of the kappa-based overload below; kept for backwards compatibility. /// @param name_ LP token name /// @param symbol_ LP token symbol /// @param _tokens token addresses (n) @@ -27,7 +28,7 @@ interface IPartyPlanner { /// @return pool Address of the newly created and initialized PartyPool /// @return lpAmount Amount of LP tokens minted to the receiver function createPool( - // Pool constructor args + // Pool constructor args (legacy) string memory name_, string memory symbol_, IERC20[] memory _tokens, @@ -45,6 +46,39 @@ interface IPartyPlanner { uint256 deadline ) external returns (PartyPool pool, uint256 lpAmount); + /// @notice Creates a new PartyPool instance and initializes it with initial deposits (kappa-based). + /// @param name_ LP token name + /// @param symbol_ LP token symbol + /// @param _tokens token addresses (n) + /// @param _bases scaling bases for each token (n) - used when converting to/from internal 64.64 amounts + /// @param _kappa liquidity parameter κ in 64.64 fixed-point used to derive b = κ * S(q) + /// @param _swapFeePpm fee in parts-per-million, taken from swap input amounts before LMSR calculations + /// @param _flashFeePpm fee in parts-per-million, taken for flash loans + /// @param _stable if true and assets.length==2, then the optimization for 2-asset stablecoin pools is activated + /// @param payer address that provides the initial token deposits + /// @param receiver address that receives the minted LP tokens + /// @param initialDeposits amounts of each token to deposit initially + /// @param deadline Reverts if nonzero and the current blocktime is later than the deadline + /// @return pool Address of the newly created and initialized PartyPool + /// @return lpAmount Amount of LP tokens minted to the receiver + function createPool( + // Pool constructor args (kappa-based) + string memory name_, + string memory symbol_, + IERC20[] memory _tokens, + uint256[] memory _bases, + int128 _kappa, + uint256 _swapFeePpm, + uint256 _flashFeePpm, + bool _stable, + // Initial deposit information + address payer, + address receiver, + uint256[] memory initialDeposits, + uint256 initialLpAmount, + uint256 deadline + ) external returns (PartyPool pool, uint256 lpAmount); + /// @notice Checks if a pool is supported /// @param pool The pool address to check /// @return bool True if the pool is supported, false otherwise diff --git a/src/IPartyPool.sol b/src/IPartyPool.sol index dd0b664..868ad29 100644 --- a/src/IPartyPool.sol +++ b/src/IPartyPool.sol @@ -70,19 +70,16 @@ interface IPartyPool is IERC20Metadata { /// @dev denominators()[i] is the base for tokens[i]. These bases are chosen by deployer and must match token decimals. function denominators() external view returns (uint256[] memory); - /// @notice Trade fraction (Q64.64) representing a reference trade size as fraction of one asset's inventory. - /// @dev Used by the LMSR stabilization logic to compute target slippage. - function tradeFrac() external view returns (int128); // ABDK 64x64 - - /// @notice Target slippage (Q64.64) applied for the reference trade size specified by tradeFrac. - function targetSlippage() external view returns (int128); // ABDK 64x64 - /// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations. function swapFeePpm() external view returns (uint256); /// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts. function flashFeePpm() external view returns (uint256); + /// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q) + /// @dev Pools are constructed with a κ value; this getter exposes the κ used by the pool. + function kappa() external view returns (int128); + /// @notice Mapping from token address => (index+1). A zero value indicates the token is not in the pool. /// @dev Use index = tokenAddressToIndexPlusOne[token] - 1 when non-zero. function tokenAddressToIndexPlusOne(IERC20) external view returns (uint); diff --git a/src/LMSRStabilized.sol b/src/LMSRStabilized.sol index bd77280..bb55f1a 100644 --- a/src/LMSRStabilized.sol +++ b/src/LMSRStabilized.sol @@ -27,8 +27,7 @@ library LMSRStabilized { function init( State storage s, int128[] memory initialQInternal, - int128 tradeFrac, - int128 targetSlippage + int128 kappa ) internal { s.nAssets = initialQInternal.length; @@ -50,8 +49,8 @@ library LMSRStabilized { console2.log("nAssets", s.nAssets); console2.log("qInternal.length", s.qInternal.length); - // Compute kappa from slippage parameters - setKappaFromSlippage(s, tradeFrac, targetSlippage); + // Set kappa directly (caller provides kappa) + s.kappa = kappa; console2.log("kappa (64x64)"); console2.logInt(s.kappa); require(s.kappa > int128(0), "LMSR: kappa>0"); @@ -827,260 +826,81 @@ library LMSRStabilized { Slippage -> b computation & resize-triggered rescale -------------------- */ - /// @notice Compute and set kappa from slippage parameters and current asset quantities - /// This should be called during initialization or when recalibrating the pool parameters - function setKappaFromSlippage( - State storage s, - int128 tradeFrac, - int128 targetSlippage - ) internal { - require(s.nAssets > 0, "LMSR: no assets"); - require(s.qInternal.length == s.nAssets, "LMSR: length mismatch"); - - int128 total = _computeSizeMetric(s.qInternal); - - // Detect degenerate "all balances equal" case: if every qInternal equals the first, - // prefer the equal-inventories closed-form to avoid taking the heterogeneous path. - bool allEqual = true; - int128 first = s.qInternal[0]; - for (uint i = 1; i < s.qInternal.length; ) { - if (s.qInternal[i] != first) { - allEqual = false; - break; - } - unchecked { i++; } - } - - int128 targetB; - if (allEqual) { - // All assets have identical internal balances -> use equal-case core explicitly. - targetB = _computeBFromSlippageCore(total, s.nAssets, tradeFrac, targetSlippage, true); - } else { - // Compute target b using representative per-asset q for improved numerical stability - targetB = _computeBFromSlippage(total, s.nAssets, tradeFrac, targetSlippage); - } - - // Numeric trace for debugging / verification - console2.log("setKappaFromSlippage: trace start"); - console2.log("Q (total, Q64.64):"); - console2.logInt(total); - console2.log("tradeFrac (f, Q64.64):"); - console2.logInt(tradeFrac); - console2.log("targetSlippage (s, Q64.64):"); - console2.logInt(targetSlippage); - console2.log("nAssets:"); - console2.logUint(s.nAssets); - console2.log("total (S(q), Q64.64):"); - console2.logInt(total); - console2.log("targetB (computed, Q64.64):"); - console2.logInt(targetB); - console2.log("setKappaFromSlippage: trace end"); - - // Compute kappa = b_target / S(q) - s.kappa = targetB.div(total); - require(s.kappa > int128(0), "LMSR: kappa<=0"); - - console2.log("Set kappa from slippage params:"); - console2.log("total"); - console2.logInt(total); - console2.log("targetB"); - console2.logInt(targetB); - console2.log("kappa"); - console2.logInt(s.kappa); - } - - /// @notice Public wrapper for computing b from slippage parameters. - /// Picks the degenerate closed-form when the heterogeneous invariants are not satisfied, - /// otherwise uses the heterogeneous derivation implemented in computeBFromSlippageCore. - function _computeBFromSlippage( - int128 q, // total assets + /// @notice Internal helper to compute kappa from slippage parameters. + /// @dev Returns κ in Q64.64. Implemented as internal so callers within the library can use it + /// without resorting to external calls. + function _computeKappaFromSlippage( uint256 nAssets, int128 tradeFrac, int128 targetSlippage - ) internal pure returns (int128) { - // Quick sanity checks that decide whether the heterogeneous formula is applicable. - // If not, fall back to the closed-form equal-asset formula for stability. - int128 onePlusS = ONE.add(targetSlippage); - - int128 n64 = ABDKMath64x64.fromUInt(nAssets); - int128 nMinus1_64 = ABDKMath64x64.fromUInt(nAssets - 1); - - // If 1 + s >= n then heterogeneous formula degenerates; use equal-asset closed-form. - if (onePlusS >= n64) { - return _computeBFromSlippageCore(q, nAssets, tradeFrac, targetSlippage, true); - } - - // denom = n - (1+s) - int128 denom = n64.sub(onePlusS); - int128 prod = onePlusS.mul(nMinus1_64); - - // If prod <= 0 or denom >= prod then heterogeneous formula is not in its valid range. - if (!(prod > int128(0) && denom < prod)) { - return _computeBFromSlippageCore(q, nAssets, tradeFrac, targetSlippage, true); - } - - // Otherwise use the heterogeneous derivation. - return _computeBFromSlippageCore(q, nAssets, tradeFrac, targetSlippage, false); - } - - /// @notice Core implementation that computes b from slippage parameters. - /// If assumeEqual == true, uses the closed-form algebra for equal inventories. - /// Otherwise uses the general derivation (original heterogeneous formula). - function _computeBFromSlippageCore( - int128 q, // total assets - uint256 nAssets, - int128 tradeFrac, - int128 targetSlippage, - bool assumeEqual ) internal pure returns (int128) { require(nAssets > 1, "LMSR: n>1 required"); - require(q > int128(0), "LMSR: q>0"); + // f must be in (0,1) int128 f = tradeFrac; require(f > int128(0), "LMSR: f=0"); require(f < ONE, "LMSR: f>=1"); - // Top-level input debug - console2.log("computeBFromSlippageCore: inputs"); - console2.log("q (64.64)"); - console2.logInt(q); - console2.log("nAssets"); - console2.logUint(nAssets); - console2.log("tradeFrac f (64.64)"); - console2.logInt(f); - console2.log("targetSlippage S (64.64)"); - console2.logInt(targetSlippage); - console2.log("assumeEqual"); - console2.logUint(assumeEqual ? 1 : 0); + int128 onePlusS = ONE.add(targetSlippage); - if (assumeEqual) { - // Closed-form equal-asset simplification for an n-asset pool: - // Let s be the target relative increase in OTHER assets' price-share when - // removing fraction f of a single asset. For equal inventories we derive: - // E = exp(-y*f) = (1 - s*(n-1)) / (1 + s) - // where y = q / b. Therefore: - // y = -ln(E) / f - // b = q / y = q * f / (-ln(E)) + int128 n64 = ABDKMath64x64.fromUInt(nAssets); + int128 nMinus1_64 = ABDKMath64x64.fromUInt(nAssets - 1); - int128 nMinus1 = ABDKMath64x64.fromUInt(nAssets - 1); - int128 numerator = ONE.sub(targetSlippage.mul(nMinus1)); // 1 - s*(n-1) - int128 denominator = ONE.add(targetSlippage); // 1 + s + // If 1 + s >= n then equal-inventories closed-form applies + bool useEqual = (onePlusS >= n64); - console2.log("equal-case intermediates:"); - console2.log("numerator = 1 - s*(n-1)"); - console2.logInt(numerator); - console2.log("denominator = 1 + s"); - console2.logInt(denominator); + // E candidate used in deriving y = -ln(E)/f (same expression in both branches) + int128 numerator = ONE.sub(targetSlippage.mul(nMinus1_64)); // 1 - s*(n-1) + int128 denominator = onePlusS; // 1 + s - require(numerator > int128(0), "LMSR: s too large for n"); // ensures ratio>0 - - int128 ratio = numerator.div(denominator); // E candidate - console2.log("E candidate (ratio = numerator/denominator)"); - console2.logInt(ratio); - - // E must be strictly between 0 and 1 for a positive y - require(ratio > int128(0) && ratio < ONE, "LMSR: bad E ratio"); - - int128 lnE = _ln(ratio); // ln(E) < 0 - console2.log("ln(E)"); - console2.logInt(lnE); - - // y = -ln(E) / f - int128 y = lnE.neg().div(f); - console2.log("y = -ln(E)/f"); - console2.logInt(y); - require(y > int128(0), "LMSR: y<=0"); - - int128 b = q.div(y); - console2.log("b = q / y (computed)"); - console2.logInt(b); - require(b > int128(0), "LMSR: b<=0"); - - // Simulate the slippage using this b to verify - int128 expArg = y.mul(f).neg(); - int128 E_sim = _exp(expArg); - int128 n64 = ABDKMath64x64.fromUInt(nAssets); - int128 nMinus1_64 = ABDKMath64x64.fromUInt(nAssets - 1); - int128 simulatedSlippage = n64.div(nMinus1_64.add(E_sim)).sub(ONE); - console2.log("simulatedSlippage (using computed b)"); - console2.logInt(simulatedSlippage); - - return b; + if (useEqual) { + // Guard numerator to ensure E in (0,1) + require(numerator > int128(0), "LMSR: s too large for n"); } else { - // Heterogeneous / general case (original derivation): - // E = exp(-y * f) where y = q / b - // and E = (1+s) * (n-1) / (n - (1+s)) - // so y = -ln(E) / f and b = q / y. - int128 onePlusS = ONE.add(targetSlippage); - - console2.log("heterogeneous intermediates:"); - console2.log("onePlusS = 1 + s"); - console2.logInt(onePlusS); - - int128 n64 = ABDKMath64x64.fromUInt(nAssets); - int128 nMinus1_64 = ABDKMath64x64.fromUInt(nAssets - 1); - - // denom = n - (1+s) - int128 denom = n64.sub(onePlusS); - console2.log("denom = n - (1+s)"); - console2.logInt(denom); - - // Guard and clamp pathological cases similar to previous logic - int128 eps = ABDKMath64x64.divu(1, 1_000_000_000); // small epsilon ~1e-9 in Q64.64 - if (onePlusS >= n64) { - console2.log('clamping'); - onePlusS = n64.sub(eps); - denom = n64.sub(onePlusS); - } - require(denom > int128(0), "LMSR: bad slippage or n"); - - int128 prod = onePlusS.mul(nMinus1_64); - console2.log("prod = (1+s)*(n-1)"); - console2.logInt(prod); - - if (!(prod > int128(0) && denom < prod)) { - if (denom >= prod) { - onePlusS = onePlusS.sub(eps); - denom = n64.sub(onePlusS); - prod = onePlusS.mul(nMinus1_64); - } - require(prod > int128(0) && denom < prod, "LMSR: slippage out of range"); - } - - // Correct E candidate for the slippage relation: - // E = (1 - s*(n-1)) / (1 + s) - int128 E_candidate = (ONE.sub(targetSlippage.mul(nMinus1_64))).div(onePlusS); - console2.log("E candidate ((1 - s*(n-1)) / (1+s))"); - console2.logInt(E_candidate); - - // Compute ln(E) directly from the ratio E_candidate for improved numerical stability - int128 lnE = _ln(E_candidate); - console2.log("lnE = ln(E_candidate)"); - console2.logInt(lnE); - - // y = -ln(E) / f - int128 y = lnE.neg().div(f); - console2.log("y = -ln(E)/f"); - console2.logInt(y); - require(y > int128(0), "LMSR: y<=0"); - - // b = q / y - int128 b = q.div(y); - console2.log("b = q / y (computed)"); - console2.logInt(b); - require(b > int128(0), "LMSR: b<=0"); - - // Simulate slippage using this b to verify - int128 expArg = y.mul(f).neg(); - int128 E_sim = _exp(expArg); - int128 simulatedSlippage = n64.div(nMinus1_64.add(E_sim)).sub(ONE); - console2.log("simulatedSlippage (heterogeneous)"); - console2.logInt(simulatedSlippage); - - return b; + // In heterogeneous logic we also require the candidate to be in range; keep same guard + require(numerator > int128(0), "LMSR: bad slippage or n"); } + + int128 E_candidate = numerator.div(denominator); + require(E_candidate > int128(0) && E_candidate < ONE, "LMSR: bad E ratio"); + + // y = -ln(E) / f + int128 lnE = _ln(E_candidate); + int128 y = lnE.neg().div(f); + require(y > int128(0), "LMSR: y<=0"); + + // kappa = 1 / y (since b = q / y -> kappa = b / q = 1 / y) + int128 kappa = ONE.div(y); + require(kappa > int128(0), "LMSR: kappa<=0"); + + return kappa; } + /// @notice Compute kappa from slippage parameters. + /// @dev External wrapper that delegates to internal implementation. + function computeKappaFromSlippage( + uint256 nAssets, + int128 tradeFrac, + int128 targetSlippage + ) external pure returns (int128) { + return _computeKappaFromSlippage(nAssets, tradeFrac, targetSlippage); + } + + /// @notice Legacy-compatible init: compute kappa from slippage parameters and delegate to kappa-based init. + /// @dev Provides backward compatibility for callers that still use the (q, tradeFrac, targetSlippage) init signature. + function init( + State storage s, + int128[] memory initialQInternal, + int128 tradeFrac, + int128 targetSlippage + ) internal { + // compute kappa using the internal helper + int128 kappa = _computeKappaFromSlippage(initialQInternal.length, tradeFrac, targetSlippage); + // forward to the new kappa-based init + init(s, initialQInternal, kappa); + } + + /// @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. function deinit(State storage s) internal { diff --git a/src/PartyPlanner.sol b/src/PartyPlanner.sol index a37b0ef..80f5fff 100644 --- a/src/PartyPlanner.sol +++ b/src/PartyPlanner.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.30; import "./IPartyPlanner.sol"; import "./PartyPool.sol"; +import "./LMSRStabilized.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -19,15 +20,14 @@ contract PartyPlanner is IPartyPlanner { mapping(IERC20 => bool) private _tokenSupported; mapping(IERC20 => PartyPool[]) private _poolsByToken; - /// @inheritdoc IPartyPlanner + /// Main createPool variant: accepts kappa directly (preferred). function createPool( // Pool constructor args string memory name_, string memory symbol_, IERC20[] memory _tokens, uint256[] memory _bases, - int128 _tradeFrac, - int128 _targetSlippage, + int128 _kappa, uint256 _swapFeePpm, uint256 _flashFeePpm, bool _stable, @@ -37,25 +37,23 @@ contract PartyPlanner is IPartyPlanner { uint256[] memory initialDeposits, uint256 initialLpAmount, uint256 deadline - ) external returns (PartyPool pool, uint256 lpAmount) { + ) public returns (PartyPool pool, uint256 lpAmount) { // Validate inputs require(deadline == 0 || block.timestamp <= deadline, "Planner: deadline exceeded"); require(_tokens.length == initialDeposits.length, "Planner: tokens and deposits length mismatch"); require(payer != address(0), "Planner: payer cannot be zero address"); require(receiver != address(0), "Planner: receiver cannot be zero address"); - // Validate fixed-point fractions: must be less than 1.0 in 64.64 fixed-point - require(_tradeFrac < FIXED_ONE_64x64, "Planner: tradeFrac must be < 1 (64.64)"); - require(_targetSlippage < FIXED_ONE_64x64, "Planner: targetSlippage must be < 1 (64.64)"); + // Validate kappa > 0 (Q64.64) + require(_kappa > int128(0), "Planner: kappa must be > 0"); - // Create a new PartyPool instance + // Create a new PartyPool instance (kappa-based constructor) pool = new PartyPool( name_, symbol_, _tokens, _bases, - _tradeFrac, - _targetSlippage, + _kappa, _swapFeePpm, _flashFeePpm, _stable @@ -90,6 +88,50 @@ contract PartyPlanner is IPartyPlanner { // Call mint on the new pool to initialize it with the transferred tokens lpAmount = pool.initialMint(receiver, initialLpAmount); } + + /// Backwards-compatible convenience overload: compute kappa from (tradeFrac, targetSlippage) then call kappa-based createPool. + function createPool( + // Pool constructor args (old signature) + string memory name_, + string memory symbol_, + IERC20[] memory _tokens, + uint256[] memory _bases, + int128 _tradeFrac, + int128 _targetSlippage, + uint256 _swapFeePpm, + uint256 _flashFeePpm, + bool _stable, + // Initial deposit information + address payer, + address receiver, + uint256[] memory initialDeposits, + uint256 initialLpAmount, + uint256 deadline + ) external returns (PartyPool pool, uint256 lpAmount) { + // Validate fixed-point fractions: must be less than 1.0 in 64.64 fixed-point + require(_tradeFrac < FIXED_ONE_64x64, "Planner: tradeFrac must be < 1 (64.64)"); + require(_targetSlippage < FIXED_ONE_64x64, "Planner: targetSlippage must be < 1 (64.64)"); + + // Compute kappa from slippage params using LMSR helper (kappa depends only on n, f and s) + int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(_tokens.length, _tradeFrac, _targetSlippage); + + // Delegate to the kappa-based createPool variant + return createPool( + name_, + symbol_, + _tokens, + _bases, + computedKappa, + _swapFeePpm, + _flashFeePpm, + _stable, + payer, + receiver, + initialDeposits, + initialLpAmount, + deadline + ); + } /// @inheritdoc IPartyPlanner function getPoolSupported(address pool) external view returns (bool) { diff --git a/src/PartyPool.sol b/src/PartyPool.sol index 1f09775..85d123e 100644 --- a/src/PartyPool.sol +++ b/src/PartyPool.sol @@ -49,12 +49,10 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { // 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 Trade fraction (Q64.64) representing a reference trade size as fraction of one asset's inventory. - /// @dev Used by the LMSR stabilization logic to compute target slippage. - int128 public immutable tradeFrac; // slippage target trade size as a fraction of one asset's inventory - - /// @notice Target slippage (Q64.64) applied for the reference trade size specified by tradeFrac. - int128 public immutable targetSlippage; // target slippage applied to that trade size + /// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q) + /// @dev Pool is constructed with a fixed κ. Clients that previously passed tradeFrac/targetSlippage + /// should use LMSRStabilized.computeKappaFromSlippage(...) to derive κ and pass it here. + int128 public immutable kappa; // kappa in Q64.64 /// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations. uint256 public immutable swapFeePpm; @@ -94,8 +92,7 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { /// @param symbol_ LP token symbol /// @param _tokens token addresses (n) /// @param _bases scaling bases for each token (n) - used when converting to/from internal 64.64 amounts - /// @param _tradeFrac trade fraction in 64.64 fixed-point (as used by LMSR) - /// @param _targetSlippage target slippage in 64.64 fixed-point (as used by LMSR) + /// @param _kappa liquidity parameter κ (Q64.64) used to derive b = κ * S(q) /// @param _swapFeePpm fee in parts-per-million, taken from swap input amounts before LMSR calculations /// @param _flashFeePpm fee in parts-per-million, taken for flash loans /// @param _stable if true and assets.length==2, then the optimization for 2-asset stablecoin pools is activated. @@ -104,8 +101,7 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { string memory symbol_, IERC20[] memory _tokens, uint256[] memory _bases, - int128 _tradeFrac, - int128 _targetSlippage, + int128 _kappa, uint256 _swapFeePpm, uint256 _flashFeePpm, bool _stable @@ -114,8 +110,7 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { require(_tokens.length == _bases.length, "Pool: lengths mismatch"); tokens = _tokens; bases = _bases; - tradeFrac = _tradeFrac; - targetSlippage = _targetSlippage; + kappa = _kappa; require(_swapFeePpm < 1_000_000, "Pool: fee >= ppm"); swapFeePpm = _swapFeePpm; require(_flashFeePpm < 1_000_000, "Pool: flash fee >= ppm"); @@ -192,8 +187,8 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard { unchecked { i++; } } - // Initialize the stabilized LMSR state - lmsr.init(newQInternal, tradeFrac, targetSlippage); + // 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 ) diff --git a/test/GasTest.sol b/test/GasTest.sol index a6acee7..8cd4ea4 100644 --- a/test/GasTest.sol +++ b/test/GasTest.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.30; import "forge-std/Test.sol"; import "@abdk/ABDKMath64x64.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "../src/LMSRStabilized.sol"; import "../src/PartyPool.sol"; // Import the flash callback interface @@ -169,7 +170,9 @@ contract GasTest is Test { for (uint i = 0; i < tokens.length; i++) { ierc20Tokens[i] = IERC20(tokens[i]); } - PartyPool newPool = new PartyPool(poolName, poolName, ierc20Tokens, bases, tradeFrac, targetSlippage, feePpm, feePpm, false); + // Compute kappa from slippage params and number of tokens, then construct pool with kappa + int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(ierc20Tokens.length, tradeFrac, targetSlippage); + PartyPool newPool = new PartyPool(poolName, poolName, ierc20Tokens, bases, computedKappa, feePpm, feePpm, false); // Transfer initial deposit amounts into pool before initial mint for (uint256 i = 0; i < numTokens; i++) { @@ -208,7 +211,8 @@ contract GasTest is Test { for (uint i = 0; i < tokens.length; i++) { ierc20Tokens[i] = IERC20(tokens[i]); } - PartyPool newPool = new PartyPool(poolName, poolName, ierc20Tokens, bases, tradeFrac, targetSlippage, feePpm, feePpm, true); + int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(ierc20Tokens.length, tradeFrac, targetSlippage); + PartyPool newPool = new PartyPool(poolName, poolName, ierc20Tokens, bases, computedKappa, feePpm, feePpm, true); // Transfer initial deposit amounts into pool before initial mint for (uint256 i = 0; i < numTokens; i++) { diff --git a/test/PartyPlanner.t.sol b/test/PartyPlanner.t.sol index f647cb2..1020eac 100644 --- a/test/PartyPlanner.t.sol +++ b/test/PartyPlanner.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.30; import "forge-std/Test.sol"; +import "../src/LMSRStabilized.sol"; import "../src/PartyPlanner.sol"; import "../src/PartyPool.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; @@ -83,14 +84,15 @@ contract PartyPlannerTest is Test { uint256 initialTokenACount = planner.poolsByTokenCount(IERC20(address(tokenA))); uint256 initialTokenBCount = planner.poolsByTokenCount(IERC20(address(tokenB))); - // Create pool + // Compute kappa then create pool via kappa overload + int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); + (PartyPool pool, uint256 lpAmount) = planner.createPool( name, symbol, tokens, bases, - tradeFrac, - targetSlippage, + computedKappa, swapFeePpm, flashFeePpm, false, // not stable @@ -163,9 +165,10 @@ contract PartyPlannerTest is Test { deposits1[0] = INITIAL_DEPOSIT_AMOUNT; deposits1[1] = INITIAL_DEPOSIT_AMOUNT; + int128 kappa1 = LMSRStabilized.computeKappaFromSlippage(tokens1.length, int128((1 << 64) - 1), int128(1 << 62)); (PartyPool pool1,) = planner.createPool( "Pool 1", "LP1", tokens1, bases1, - int128((1 << 64) - 1), int128(1 << 62), 3000, 5000, false, + kappa1, 3000, 5000, false, payer, receiver, deposits1, 1000e18, 0 ); @@ -182,9 +185,10 @@ contract PartyPlannerTest is Test { deposits2[0] = INITIAL_DEPOSIT_AMOUNT; deposits2[1] = INITIAL_DEPOSIT_AMOUNT / 1e12; // Adjust for 6 decimals + int128 kappa2 = LMSRStabilized.computeKappaFromSlippage(tokens2.length, int128((1 << 64) - 1), int128(1 << 62)); (PartyPool pool2,) = planner.createPool( "Pool 2", "LP2", tokens2, bases2, - int128((1 << 64) - 1), int128(1 << 62), 3000, 5000, false, + kappa2, 3000, 5000, false, payer, receiver, deposits2, 1000e18, 0 ); @@ -225,6 +229,7 @@ contract PartyPlannerTest is Test { // Test token/deposit length mismatch vm.expectRevert("Planner: tokens and deposits length mismatch"); + // call old-signature convenience (it will still exist) for the mismatched-length revert check planner.createPool( "Test Pool", "TESTLP", tokens, bases, int128((1 << 64) - 1), int128(1 << 62), 3000, 5000, false, @@ -236,10 +241,12 @@ contract PartyPlannerTest is Test { validDeposits[0] = INITIAL_DEPOSIT_AMOUNT; validDeposits[1] = INITIAL_DEPOSIT_AMOUNT; + int128 kappaErr = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62)); + vm.expectRevert("Planner: payer cannot be zero address"); planner.createPool( "Test Pool", "TESTLP", tokens, bases, - int128((1 << 64) - 1), int128(1 << 62), 3000, 5000, false, + kappaErr, 3000, 5000, false, address(0), receiver, validDeposits, 1000e18, 0 ); @@ -247,17 +254,18 @@ contract PartyPlannerTest is Test { vm.expectRevert("Planner: receiver cannot be zero address"); planner.createPool( "Test Pool", "TESTLP", tokens, bases, - int128((1 << 64) - 1), int128(1 << 62), 3000, 5000, false, + kappaErr, 3000, 5000, false, payer, address(0), validDeposits, 1000e18, 0 ); // Test deadline exceeded // The default timestamp is 1 and 1-0 is 0 which means "ignore deadline," so we need to set a proper timestamp. + int128 kappaDeadline = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62)); vm.warp(1000); vm.expectRevert("Planner: deadline exceeded"); planner.createPool( "Test Pool", "TESTLP", tokens, bases, - int128((1 << 64) - 1), int128(1 << 62), 3000, 5000, false, + kappaDeadline, 3000, 5000, false, payer, receiver, validDeposits, 1000e18, block.timestamp - 1 ); } @@ -280,11 +288,12 @@ contract PartyPlannerTest is Test { deposits[0] = INITIAL_DEPOSIT_AMOUNT; deposits[1] = INITIAL_DEPOSIT_AMOUNT; + int128 kappaLoop = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62)); (PartyPool pool,) = planner.createPool( string(abi.encodePacked("Pool ", vm.toString(i))), string(abi.encodePacked("LP", vm.toString(i))), tokens, bases, - int128((1 << 64) - 1), int128(1 << 62), 3000, 5000, false, + kappaLoop, 3000, 5000, false, payer, receiver, deposits, 1000e18, 0 ); diff --git a/test/PartyPool.t.sol b/test/PartyPool.t.sol index f4c6d53..3f60345 100644 --- a/test/PartyPool.t.sol +++ b/test/PartyPool.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.30; import "forge-std/Test.sol"; import "@abdk/ABDKMath64x64.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "../src/LMSRStabilized.sol"; import "../src/PartyPool.sol"; // Import the flash callback interface @@ -195,7 +196,8 @@ contract PartyPoolTest is Test { // Deploy pool with a small fee to test fee-handling paths (use 1000 ppm = 0.1%) uint256 feePpm = 1000; - pool = new PartyPool("LP", "LP", tokens, bases, tradeFrac, targetSlippage, feePpm, feePpm, false); + int128 kappa3 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); + pool = new PartyPool("LP", "LP", tokens, bases, kappa3, feePpm, feePpm, false); // Transfer initial deposit amounts into pool before initial mint (pool expects tokens already in contract) // We deposit equal amounts INIT_BAL for each token @@ -224,7 +226,8 @@ contract PartyPoolTest is Test { bases10[i] = BASE; } - pool10 = new PartyPool("LP10", "LP10", tokens10, bases10, tradeFrac, targetSlippage, feePpm, feePpm, false); + int128 kappa10 = LMSRStabilized.computeKappaFromSlippage(tokens10.length, tradeFrac, targetSlippage); + pool10 = new PartyPool("LP10", "LP10", tokens10, bases10, kappa10, feePpm, feePpm, false); // Mint additional tokens for pool10 initial deposit token0.mint(address(this), INIT_BAL); @@ -1227,10 +1230,12 @@ contract PartyPoolTest is Test { uint256 feePpm = 1000; // Pool with default initialization (lpTokens = 0) - PartyPool poolDefault = new PartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, tradeFrac, targetSlippage, feePpm, feePpm, false); + int128 kappaDefault = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); + PartyPool poolDefault = new PartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault, feePpm, feePpm, false); // Pool with custom initialization (lpTokens = custom amount) - PartyPool poolCustom = new PartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, tradeFrac, targetSlippage, feePpm, feePpm, false); + int128 kappaCustom = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); + PartyPool poolCustom = new PartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom, feePpm, feePpm, false); // Mint additional tokens for both pools token0.mint(address(this), INIT_BAL * 2); @@ -1301,8 +1306,10 @@ contract PartyPoolTest is Test { uint256 feePpm = 1000; - PartyPool poolDefault = new PartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, tradeFrac, targetSlippage, feePpm, feePpm, false); - PartyPool poolCustom = new PartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, tradeFrac, targetSlippage, feePpm, feePpm, false); + int128 kappaDefault2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); + PartyPool poolDefault = new PartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault2, feePpm, feePpm, false); + int128 kappaCustom2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); + PartyPool poolCustom = new PartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom2, feePpm, feePpm, false); // Mint additional tokens token0.mint(address(this), INIT_BAL * 4);