Compare commits
1 Commits
main
...
precompute
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc7df27676 |
261
src/LMSRBalancedPairPrecomputed.sol
Normal file
261
src/LMSRBalancedPairPrecomputed.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_,
|
||||
|
||||
Reference in New Issue
Block a user