pure kappa formulation: target slippage extracted into pool creator code

This commit is contained in:
tim
2025-09-23 19:08:14 -04:00
parent cd663105f4
commit b5eab7dad1
8 changed files with 198 additions and 290 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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 )

View File

@@ -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++) {

View File

@@ -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
);

View File

@@ -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);