1 Commits

4 changed files with 328 additions and 25 deletions

View File

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

View File

@@ -1,15 +1,20 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30; 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 {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {LMSRBalancedPairPrecomputed} from "./LMSRBalancedPairPrecomputed.sol";
import {NativeWrapper} from "./NativeWrapper.sol"; import {NativeWrapper} from "./NativeWrapper.sol";
import {LMSRStabilizedBalancedPair} from "./LMSRStabilizedBalancedPair.sol";
import {PartyPool} from "./PartyPool.sol"; import {PartyPool} from "./PartyPool.sol";
import {PartyPoolBase} from "./PartyPoolBase.sol"; import {PartyPoolBase} from "./PartyPoolBase.sol";
import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol"; import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol";
import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol"; import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol";
contract PartyPoolBalancedPair is PartyPool { contract PartyPoolBalancedPair is PartyPool {
LMSRBalancedPairPrecomputed.Precomp internal _precomp;
bool internal _precomputed;
constructor( constructor(
address owner_, address owner_,
string memory name_, string memory name_,
@@ -19,17 +24,32 @@ contract PartyPoolBalancedPair is PartyPool {
int128 kappa_, int128 kappa_,
uint256 swapFeePpm_, uint256 swapFeePpm_,
uint256 flashFeePpm_, uint256 flashFeePpm_,
uint256 protocolFeePpm_, // NEW: protocol share of fees (ppm) uint256 protocolFeePpm_,
address protocolFeeAddress_, // NEW: recipient for collected protocol tokens address protocolFeeAddress_,
NativeWrapper wrapperToken_, NativeWrapper wrapperToken_,
PartyPoolSwapImpl swapMintImpl_, PartyPoolSwapImpl swapMintImpl_,
PartyPoolMintImpl mintImpl_ PartyPoolMintImpl mintImpl_
) )
PartyPool(owner_, name_, symbol_, tokens_, bases_, kappa_, swapFeePpm_, flashFeePpm_, protocolFeePpm_, protocolFeeAddress_, wrapperToken_, swapMintImpl_, 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 function _swapAmountsForExactInput(uint256 i, uint256 j, int128 a, int128 limitPrice) internal virtual override view
returns (int128 amountIn, int128 amountOut) { 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);
} }
} }

View File

@@ -1,10 +1,14 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30; pragma solidity ^0.8.30;
import "./PartyPoolMintImpl.sol"; import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
import "./PartyPoolSwapImpl.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 {PartyPool} from "./PartyPool.sol";
import {PartyPoolBalancedPair} from "./PartyPoolBalancedPair.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) // 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. // 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_, PartyPoolSwapImpl swapImpl_,
PartyPoolMintImpl mintImpl_ PartyPoolMintImpl mintImpl_
) external returns (IPartyPool) { ) external returns (IPartyPool) {
return new PartyPoolBalancedPair( PartyPoolBalancedPair pool = new PartyPoolBalancedPair(
owner_, owner_,
name_, name_,
symbol_, symbol_,
@@ -92,5 +96,14 @@ contract PartyPoolBalancedPairDeployer is IPartyPoolDeployer {
swapImpl_, swapImpl_,
mintImpl_ 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;
} }
} }

View File

@@ -1,6 +1,7 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30; 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 {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {NativeWrapper} from "../src/NativeWrapper.sol"; import {NativeWrapper} from "../src/NativeWrapper.sol";
import {PartyPlanner} from "../src/PartyPlanner.sol"; import {PartyPlanner} from "../src/PartyPlanner.sol";
@@ -66,23 +67,31 @@ library Deploy {
NativeWrapper wrapper, NativeWrapper wrapper,
bool _stable bool _stable
) internal returns (PartyPool) { ) internal returns (PartyPool) {
return _stable && tokens_.length == 2 ? if(_stable && tokens_.length == 2) {
new PartyPoolBalancedPair( PartyPoolBalancedPair pool = new PartyPoolBalancedPair(
owner_, owner_,
name_, name_,
symbol_, symbol_,
tokens_, tokens_,
bases_, bases_,
_kappa, _kappa,
_swapFeePpm, _swapFeePpm,
_flashFeePpm, _flashFeePpm,
PROTOCOL_FEE_PPM, PROTOCOL_FEE_PPM,
PROTOCOL_FEE_RECEIVER, PROTOCOL_FEE_RECEIVER,
wrapper, wrapper,
new PartyPoolSwapImpl(wrapper), new PartyPoolSwapImpl(wrapper),
new PartyPoolMintImpl(wrapper) new PartyPoolMintImpl(wrapper)
) : );
new PartyPool( 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_, owner_,
name_, name_,
symbol_, symbol_,