diff --git a/src/LMSRBalancedPairPrecomputed.sol b/src/LMSRBalancedPairPrecomputed.sol new file mode 100644 index 0000000..c29adbb --- /dev/null +++ b/src/LMSRBalancedPairPrecomputed.sol @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.30; + +import "forge-std/console2.sol"; +import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol"; +import {LMSRStabilized} from "./LMSRStabilized.sol"; + +/// @title LMSRBalancedPairPrecomputed +/// @notice Balanced-pair LMSR using a precomputed lookup table for amount-out: +/// y = b * ln(1 + α e^Δ), with Δ = (q_i - q_j)/b and α = 1 - e^{-u}, u = a/b. +/// @dev Precomputation is done once with exp/ln; runtime swaps are interpolation-only with no transcendental ops. +library LMSRBalancedPairPrecomputed { + using ABDKMath64x64 for int128; + using LMSRStabilized for LMSRStabilized.State; + + // Q64.64 constants + int128 private constant ONE = 0x10000000000000000; + + /// @dev Precomputation-only state; keeps table configuration and data separate from LMSR core state. + struct Precomp { + // Precompute config + int128 W; // half-width for Δ band (Q64.64), table centered at 0 + int128 uMax; // maximum u = a/b covered (Q64.64) + uint256 NDelta; // number of Δ samples (>= 2) + uint256 NAlpha; // number of u/α samples (>= 2) + int128 deltaStep; // step in Δ grid (Q64.64) == 2W/(NDelta-1) + int128 uStep; // step in u grid (Q64.64) == uMax/(NAlpha-1) + + // Grids and tables + int128[] deltaGrid; // Δ_i from -W to +W, length NDelta + int128[] uGrid; // u_j from 0 to uMax, length NAlpha + int128[] alphaLut; // α(u_j) = 1 - exp(-u_j), length NAlpha + int128[][] g; // g[j][i] = ln(1 + α_j * exp(Δ_i)), size NAlpha x NDelta + } + + /* -------------------- + Initialization + -------------------- */ + + /// @notice Initialize the precomputed tables (assumes plenty of gas). + /// @param s LMSR core state (must already represent a 2-asset pool with kappa set) + /// @param p Precomputed table state to be filled + /// @param W Half-width in Δ around parity (Q64.64), table covers Δ ∈ [-W, +W] + /// @param uMax Maximum u = a/b to be supported by the table (Q64.64) + /// @param NDelta Number of Δ samples (>= 2) + /// @param NAlpha Number of u/α samples (>= 2) + function initPrecomputed( + LMSRStabilized.State storage s, + Precomp storage p, + int128 W, + int128 uMax, + uint256 NDelta, + uint256 NAlpha + ) internal { + require(s.nAssets == 2, "Precomp: requires 2-asset pool"); + require(s.kappa > int128(0), "Precomp: kappa>0"); + require(W > int128(0), "Precomp: W>0"); + require(uMax > int128(0), "Precomp: uMax>0"); + require(NDelta >= 2, "Precomp: NDelta>=2"); + require(NAlpha >= 2, "Precomp: NAlpha>=2"); + + // Store config + p.W = W; + p.uMax = uMax; + p.NDelta = NDelta; + p.NAlpha = NAlpha; + + // Steps + p.deltaStep = _div64(_mul64(W, _fromUInt(2)), _fromUInt(NDelta - 1)); // 2W/(NDelta-1) + p.uStep = _div64(uMax, _fromUInt(NAlpha - 1)); // uMax/(NAlpha-1) + + // Build grids + p.deltaGrid = new int128[](NDelta); + p.uGrid = new int128[](NAlpha); + p.alphaLut = new int128[](NAlpha); + p.g = new int128[][](NAlpha); + + // Δ grid from -W to +W + int128 startDelta = W.neg(); + for (uint256 i = 0; i < NDelta; ) { + p.deltaGrid[i] = startDelta.add(_mul64(p.deltaStep, _fromUInt(i))); + unchecked { ++i; } + } + + // Build u grid and alpha(u) LUT + for (uint256 j = 0; j < NAlpha; ) { + int128 uj = _mul64(p.uStep, _fromUInt(j)); // 0 .. uMax + p.uGrid[j] = uj; + // α(u) = 1 - exp(-u) + int128 alphaJ = ONE.sub(ABDKMath64x64.exp(uj.neg())); + p.alphaLut[j] = alphaJ; + p.g[j] = new int128[](NDelta); + unchecked { ++j; } + } + + // Fill g[j][i] = ln(1 + α_j * exp(Δ_i)) + for (uint256 j = 0; j < NAlpha; ) { + int128 alphaJ = p.alphaLut[j]; + for (uint256 i = 0; i < NDelta; ) { + int128 eDelta = ABDKMath64x64.exp(p.deltaGrid[i]); + int128 inner = ONE.add(alphaJ.mul(eDelta)); + p.g[j][i] = ABDKMath64x64.ln(inner); + unchecked { ++i; } + } + unchecked { ++j; } + } + } + + /* -------------------- + Swap (Interpolated) + -------------------- */ + + /// @notice Exact-input swap i -> j using the precomputed table when feasible. + /// @dev Falls back to exact LMSR for: + /// - limitPrice > 0, + /// - |Δ| > W or u not in [0, uMax], + /// - non-positive amounts or degenerate b, + /// - non-2-asset pools. + function swapAmountsForExactInput( + LMSRStabilized.State storage s, + Precomp storage p, + uint256 i, + uint256 j, + int128 a, + int128 limitPrice + ) internal view returns (int128 amountIn, int128 amountOut) { + // Validate pool shape and indices + if (s.nAssets != 2 || i >= 2 || j >= 2) { + console2.log('Precomp: fallback (nAssets)'); + return LMSRStabilized.swapAmountsForExactInput(s.nAssets, s.kappa, s.qInternal, i, j, a, limitPrice); + } + + // Do not handle limitPrice within the table path; use exact routine for correctness. + if (limitPrice > int128(0)) { + console2.log('Precomp: fallback (limit>0)'); + return LMSRStabilized.swapAmountsForExactInput(s.nAssets, s.kappa, s.qInternal, i, j, a, limitPrice); + } + + // Compute b = κ * S(q) + int128 sizeMetric = _computeSizeMetric(s.qInternal); + if (!(sizeMetric > int128(0))) { + console2.log('Precomp: fallback (size=0)'); + return (int128(0), int128(0)); + } + int128 b = s.kappa.mul(sizeMetric); + if (!(b > int128(0))) { + console2.log('Precomp: fallback (b<=0)'); + return (int128(0), int128(0)); + } + int128 invB = _div64(ONE, b); + + // Require positive input + if (a <= int128(0)) { + console2.log('Precomp: fallback (a<0)'); + return LMSRStabilized.swapAmountsForExactInput(s.nAssets, s.kappa, s.qInternal, i, j, a, limitPrice); + } + + // Compute u and Δ + int128 u = a.mul(invB); + if (u < int128(0) || u > p.uMax) { + // outside u range - fallback + console2.log('Precomp: fallback (outside u)'); + console2.log(u); + console2.log(p.uMax); + return LMSRStabilized.swapAmountsForExactInput(s.nAssets, s.kappa, s.qInternal, i, j, a, limitPrice); + } + + int128 delta = s.qInternal[i].sub(s.qInternal[j]).mul(invB); + int128 absDelta = delta >= int128(0) ? delta : delta.neg(); + if (absDelta > p.W) { + // outside ±W band - fallback + console2.log('Precomp: fallback (outside W)'); + return LMSRStabilized.swapAmountsForExactInput(s.nAssets, s.kappa, s.qInternal, i, j, a, limitPrice); + } + + // 1D interpolation position for u (row selection) + (uint256 j0, int128 wu) = _interpPosition(u, p.uStep, p.NAlpha); + int128 oneMinusWu = ONE.sub(wu); + + // Map Δ ∈ [-W, +W] to shifted in [0, 2W] + int128 deltaShifted = delta.add(p.W); + (uint256 i0, int128 wd) = _interpPosition(deltaShifted, p.deltaStep, p.NDelta); + int128 oneMinusWd = ONE.sub(wd); + + // Interpolate along Δ within rows j0 and j0+1 + int128 g00 = p.g[j0][i0]; + int128 g01 = p.g[j0][i0 + 1]; + int128 g0 = g00.mul(oneMinusWd).add(g01.mul(wd)); + + int128 g10 = p.g[j0 + 1][i0]; + int128 g11 = p.g[j0 + 1][i0 + 1]; + int128 g1 = g10.mul(oneMinusWd).add(g11.mul(wd)); + + // Interpolate across u between rows + int128 g = g0.mul(oneMinusWu).add(g1.mul(wu)); + + // amountOut = b * g + int128 out64 = b.mul(g); + if (out64 <= int128(0)) { + // Numerical guard; fallback + console2.log('Precomp: fallback (out<0)'); + return LMSRStabilized.swapAmountsForExactInput(s.nAssets, s.kappa, s.qInternal, i, j, a, limitPrice); + } + + console2.log('Precomp: success'); + amountIn = a; + amountOut = out64; + return (amountIn, amountOut); + } + + /* -------------------- + Internal helpers + -------------------- */ + + /// @dev Compute size metric S(q) = sum q_i + function _computeSizeMetric(int128[] memory qInternal) private pure returns (int128) { + int128 total = int128(0); + for (uint256 k = 0; k < qInternal.length; ) { + total = total.add(qInternal[k]); + unchecked { ++k; } + } + return total; + } + + /// @dev Q64.64 wrappers (avoid name conflicts and keep intent explicit) + function _fromUInt(uint256 x) private pure returns (int128) { + return ABDKMath64x64.fromUInt(x); + } + function _mul64(int128 a, int128 b) private pure returns (int128) { + return a.mul(b); + } + function _div64(int128 a, int128 b) private pure returns (int128) { + return ABDKMath64x64.div(a, b); + } + + /// @dev Given x >= 0 and a uniform step > 0, compute index floor(x/step) clamped to [0, n-2] and fraction w in [0,1]: + /// x ≈ (idx * step) * (1-w) + ((idx+1) * step) * w + function _interpPosition(int128 x, int128 step, uint256 n) + private + pure + returns (uint256 idx, int128 w) + { + // t = x/step in Q64.64 + int128 t = _div64(x, step); + // floor index = uint(t) via right shift by 64 (x >= 0 ensures non-negative) + uint256 idxFloor = uint256(int256(t)) >> 64; + if (idxFloor >= n - 1) { + // clamp to the last segment + idx = n - 2; + w = ONE; // exact at upper bound + return (idx, w); + } + idx = idxFloor; + + // fractional part: w = (x - idx*step) / step + int128 xi = _mul64(step, _fromUInt(idx)); + int128 frac = x.sub(xi); + w = _div64(frac, step); // in [0,1) + return (idx, w); + } +} diff --git a/src/PartyPoolBalancedPair.sol b/src/PartyPoolBalancedPair.sol index 80f04ce..233437d 100644 --- a/src/PartyPoolBalancedPair.sol +++ b/src/PartyPoolBalancedPair.sol @@ -1,15 +1,20 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.30; +import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol"; import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {LMSRBalancedPairPrecomputed} from "./LMSRBalancedPairPrecomputed.sol"; import {NativeWrapper} from "./NativeWrapper.sol"; -import {LMSRStabilizedBalancedPair} from "./LMSRStabilizedBalancedPair.sol"; import {PartyPool} from "./PartyPool.sol"; import {PartyPoolBase} from "./PartyPoolBase.sol"; import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol"; import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol"; contract PartyPoolBalancedPair is PartyPool { + + LMSRBalancedPairPrecomputed.Precomp internal _precomp; + bool internal _precomputed; + constructor( address owner_, string memory name_, @@ -19,17 +24,32 @@ contract PartyPoolBalancedPair is PartyPool { int128 kappa_, uint256 swapFeePpm_, uint256 flashFeePpm_, - uint256 protocolFeePpm_, // NEW: protocol share of fees (ppm) - address protocolFeeAddress_, // NEW: recipient for collected protocol tokens + uint256 protocolFeePpm_, + address protocolFeeAddress_, NativeWrapper wrapperToken_, PartyPoolSwapImpl swapMintImpl_, PartyPoolMintImpl mintImpl_ ) PartyPool(owner_, name_, symbol_, tokens_, bases_, kappa_, swapFeePpm_, flashFeePpm_, protocolFeePpm_, protocolFeeAddress_, wrapperToken_, swapMintImpl_, mintImpl_) - {} + { + // Initialize kappa early so precompute() can be called before initialMint() + _lmsr.kappa = kappa_; + } + + function precompute( + int128 W, + int128 uMax, + uint256 NDelta, + uint256 NAlpha + ) external { + require(!_precomputed, 'Precomp: already initialized'); + LMSRBalancedPairPrecomputed.initPrecomputed(_lmsr, _precomp, W, uMax, NDelta, NAlpha ); + _precomputed = true; + } function _swapAmountsForExactInput(uint256 i, uint256 j, int128 a, int128 limitPrice) internal virtual override view returns (int128 amountIn, int128 amountOut) { - return LMSRStabilizedBalancedPair.swapAmountsForExactInput(_lmsr, i, j, a, limitPrice); +// return LMSRStabilizedBalancedPair.swapAmountsForExactInput(_lmsr, i, j, a, limitPrice); + return LMSRBalancedPairPrecomputed.swapAmountsForExactInput(_lmsr, _precomp, i, j, a, limitPrice); } } diff --git a/src/PartyPoolDeployer.sol b/src/PartyPoolDeployer.sol index ac297e1..16d4f74 100644 --- a/src/PartyPoolDeployer.sol +++ b/src/PartyPoolDeployer.sol @@ -1,10 +1,14 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.30; -import "./PartyPoolMintImpl.sol"; -import "./PartyPoolSwapImpl.sol"; +import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol"; +import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {IPartyPool} from "./IPartyPool.sol"; +import {NativeWrapper} from "./NativeWrapper.sol"; import {PartyPool} from "./PartyPool.sol"; import {PartyPoolBalancedPair} from "./PartyPoolBalancedPair.sol"; +import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol"; +import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol"; // This pattern is needed because the PartyPlanner constructs two different types of pools (regular and balanced-pair) // but doesn't have room to store the initialization code of both contracts. Therefore, we delegate pool construction. @@ -77,7 +81,7 @@ contract PartyPoolBalancedPairDeployer is IPartyPoolDeployer { PartyPoolSwapImpl swapImpl_, PartyPoolMintImpl mintImpl_ ) external returns (IPartyPool) { - return new PartyPoolBalancedPair( + PartyPoolBalancedPair pool = new PartyPoolBalancedPair( owner_, name_, symbol_, @@ -92,5 +96,14 @@ contract PartyPoolBalancedPairDeployer is IPartyPoolDeployer { swapImpl_, mintImpl_ ); + + pool.precompute( + ABDKMath64x64.divu(25,10_000), // ±25 bps + ABDKMath64x64.divu(10, 100), // taking up to 10% of the pool + 20, // num Δ samples + 20 // num α samples + ); + + return pool; } } diff --git a/test/Deploy.sol b/test/Deploy.sol index 4ceece1..2e0069c 100644 --- a/test/Deploy.sol +++ b/test/Deploy.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.30; +import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol"; import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {NativeWrapper} from "../src/NativeWrapper.sol"; import {PartyPlanner} from "../src/PartyPlanner.sol"; @@ -66,23 +67,31 @@ library Deploy { NativeWrapper wrapper, bool _stable ) internal returns (PartyPool) { - return _stable && tokens_.length == 2 ? - new PartyPoolBalancedPair( - owner_, - name_, - symbol_, - tokens_, - bases_, - _kappa, - _swapFeePpm, - _flashFeePpm, - PROTOCOL_FEE_PPM, - PROTOCOL_FEE_RECEIVER, - wrapper, - new PartyPoolSwapImpl(wrapper), - new PartyPoolMintImpl(wrapper) - ) : - new PartyPool( + if(_stable && tokens_.length == 2) { + PartyPoolBalancedPair pool = new PartyPoolBalancedPair( + owner_, + name_, + symbol_, + tokens_, + bases_, + _kappa, + _swapFeePpm, + _flashFeePpm, + PROTOCOL_FEE_PPM, + PROTOCOL_FEE_RECEIVER, + wrapper, + new PartyPoolSwapImpl(wrapper), + new PartyPoolMintImpl(wrapper) + ); + pool.precompute( + ABDKMath64x64.divu(25,10_000), // ±25 bps + ABDKMath64x64.divu(10, 100), // taking up to 10% of the pool + 20, // num Δ samples + 20 // num α samples + ); + return pool; + } + return new PartyPool( owner_, name_, symbol_,