193 lines
8.6 KiB
Solidity
193 lines
8.6 KiB
Solidity
// SPDX-License-Identifier: UNLICENSED
|
|
pragma solidity ^0.8.30;
|
|
|
|
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
|
|
import {LMSRStabilized} from "./LMSRStabilized.sol";
|
|
|
|
/// @notice Specialized functions for the 2-asset stablecoin case
|
|
library LMSRStabilizedBalancedPair {
|
|
using ABDKMath64x64 for int128;
|
|
|
|
// Precomputed Q64.64 representation of 1.0 (1 << 64).
|
|
int128 private constant ONE = 0x10000000000000000;
|
|
|
|
/// @notice Specialized 2-asset balanced approximation of swapAmountsForExactInput.
|
|
/// - Assumes exactly two assets and that the two assets' internal balances are within ~1% of parity.
|
|
/// - Implements a gas-optimized two-tier Taylor approximation to avoid most exp()/ln() calls:
|
|
/// * Tier 1 (quadratic, cheapest): for small u = a/b (u <= 0.1) we compute
|
|
/// X = u*(1 + δ) - u^2/2
|
|
/// ln(1+X) ≈ X - X^2/2
|
|
/// and return amountOut ≈ b * lnApprox. This Horner-style form minimizes multiplies/divides
|
|
/// and temporaries compared to the earlier a^2/a^3 expansion.
|
|
/// * Tier 2 (cubic correction): for moderate u (0.1 < u <= 0.5) we add the X^3/3 term:
|
|
/// ln(1+X) ≈ X - X^2/2 + X^3/3
|
|
/// which improves accuracy while still being significantly cheaper than full exp/ln.
|
|
/// - For cases where |δ| (the per-asset imbalance scaled by b) or u are outside the safe ranges,
|
|
/// or when limitPrice handling cannot be reliably approximated, the function falls back to the
|
|
/// numerically-exact swapAmountsForExactInput(...) implementation to preserve correctness.
|
|
/// - The goal is to keep relative error well below 0.001% in the intended small-u, near-parity regime,
|
|
/// while substantially reducing gas in the common fast path.
|
|
function swapAmountsForExactInput(
|
|
LMSRStabilized.State storage s,
|
|
uint256 i,
|
|
uint256 j,
|
|
int128 a,
|
|
int128 limitPrice
|
|
) internal view returns (int128 amountIn, int128 amountOut) {
|
|
// Quick index check
|
|
require(i < s.nAssets && j < s.nAssets, "LMSR: idx");
|
|
|
|
// If not exactly a two-asset pool, fall back to the general routine.
|
|
if (s.nAssets != 2) {
|
|
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
|
}
|
|
|
|
// Compute b and inverse early (needed to evaluate delta and limit-price)
|
|
int128 b = LMSRStabilized._computeB(s);
|
|
// Guard: if b not positive, fallback to exact implementation (will revert there if necessary)
|
|
if (!(b > int128(0))) {
|
|
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
|
}
|
|
int128 invB = ABDKMath64x64.div(ONE, b);
|
|
|
|
// Small-signal delta = (q_i - q_j) / b (used to approximate r0 = exp(delta))
|
|
int128 delta = s.qInternal[i].sub(s.qInternal[j]).mul(invB);
|
|
|
|
// If a positive limitPrice is given, attempt a 2-asset near-parity polynomial solution
|
|
if (limitPrice > int128(0)) {
|
|
// Approximate r0 = exp(delta) using Taylor: 1 + δ + δ^2/2 + δ^3/6
|
|
int128 delta_sq = delta.mul(delta);
|
|
int128 delta_cu = delta_sq.mul(delta);
|
|
int128 r0_approx = ONE
|
|
.add(delta)
|
|
.add(delta_sq.div(ABDKMath64x64.fromUInt(2)))
|
|
.add(delta_cu.div(ABDKMath64x64.fromUInt(6)));
|
|
|
|
// If limitPrice <= r0 (current price) we must revert (same semantic as original)
|
|
if (limitPrice <= r0_approx) {
|
|
revert("LMSR: limitPrice <= current price");
|
|
}
|
|
|
|
// Ratio = limitPrice / r0_approx
|
|
int128 ratio = limitPrice.div(r0_approx);
|
|
|
|
// x = ratio - 1; use Taylor for ln(1+x) when |x| is small
|
|
int128 x = ratio.sub(ONE);
|
|
int128 absX = x >= int128(0) ? x : x.neg();
|
|
|
|
// Acceptable range for ln Taylor approx: |x| <= 0.1 (conservative)
|
|
int128 X_MAX = ABDKMath64x64.divu(1, 10); // 0.1
|
|
if (absX > X_MAX) {
|
|
// Too large to safely approximate; fall back to exact computation
|
|
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
|
}
|
|
|
|
// ln(1+x) ≈ x - x^2/2 + x^3/3
|
|
int128 x_sq = x.mul(x);
|
|
int128 x_cu = x_sq.mul(x);
|
|
int128 lnRatioApprox = x
|
|
.sub(x_sq.div(ABDKMath64x64.fromUInt(2)))
|
|
.add(x_cu.div(ABDKMath64x64.fromUInt(3)));
|
|
|
|
// aLimitOverB = ln(limitPrice / r0) approximated
|
|
int128 aLimitOverB = lnRatioApprox;
|
|
|
|
// Must be > 0; otherwise fall back
|
|
if (!(aLimitOverB > int128(0))) {
|
|
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
|
}
|
|
|
|
// aLimit = b * aLimitOverB (in Q64.64)
|
|
int128 aLimit64 = b.mul(aLimitOverB);
|
|
|
|
// If computed aLimit is less than requested a, use the truncated value.
|
|
if (aLimit64 < a) {
|
|
a = aLimit64;
|
|
} else {
|
|
// console2.log("balanced2: limitPrice does not truncate input");
|
|
}
|
|
|
|
// Note: after potential truncation we continue with the polynomial approximation below
|
|
}
|
|
|
|
// Small-signal delta already computed above; reuse it
|
|
int128 absDelta = delta >= int128(0) ? delta : delta.neg();
|
|
|
|
// Allow balanced pools only: require |delta| <= 1% (approx ln(1.01) ~ 0.00995; we use conservative 0.01)
|
|
int128 DELTA_MAX = ABDKMath64x64.divu(1, 100); // 0.01
|
|
if (absDelta > DELTA_MAX) {
|
|
// Not balanced within 1% -> use exact routine
|
|
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
|
}
|
|
|
|
// Scaled input u = a / b (Q64.64). For polynomial approximation we require moderate u.
|
|
int128 u = a.mul(invB);
|
|
if (u <= int128(0)) {
|
|
// Non-positive input -> behave like exact implementation (will revert if invalid)
|
|
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
|
}
|
|
|
|
// Restrict to a conservative polynomial radius for accuracy; fallback otherwise.
|
|
// We choose u <= 0.5 (0.5 in Q64.64) as safe for cubic approximation in typical parameters.
|
|
int128 U_MAX = ABDKMath64x64.divu(1, 2); // 0.5
|
|
if (u > U_MAX) {
|
|
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
|
}
|
|
|
|
// Now compute a two-tier approximation using Horner-style evaluation to reduce mul/divs.
|
|
// Primary tier (cheap quadratic): accurate for small u = a/b.
|
|
// Secondary tier (cubic correction): used when u is moderate but still within U_MAX.
|
|
|
|
// Precomputed thresholds
|
|
int128 U_TIER1 = ABDKMath64x64.divu(1, 10); // 0.1 -> cheap quadratic tier
|
|
int128 U_MAX_LOCAL = ABDKMath64x64.divu(1, 2); // 0.5 -> still allowed cubic tier
|
|
|
|
// u is already computed above
|
|
// Compute X = u*(1 + delta) - u^2/2
|
|
int128 u2 = u.mul(u);
|
|
int128 X = u.mul(ONE.add(delta)).sub(u2.div(ABDKMath64x64.fromUInt(2)));
|
|
|
|
// Compute X^2 once
|
|
int128 X2 = X.mul(X);
|
|
|
|
int128 lnApprox;
|
|
if (u <= U_TIER1) {
|
|
// Cheap quadratic ln(1+X) ≈ X - X^2/2
|
|
lnApprox = X.sub(X2.div(ABDKMath64x64.fromUInt(2)));
|
|
} else if (u <= U_MAX_LOCAL) {
|
|
// Secondary cubic correction: ln(1+X) ≈ X - X^2/2 + X^3/3
|
|
int128 X3 = X2.mul(X);
|
|
lnApprox = X.sub(X2.div(ABDKMath64x64.fromUInt(2))).add(X3.div(ABDKMath64x64.fromUInt(3)));
|
|
} else {
|
|
// u beyond allowed range - fallback
|
|
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
|
}
|
|
|
|
int128 approxOut = b.mul(lnApprox);
|
|
|
|
// Safety sanity: approximation must be > 0
|
|
if (approxOut <= int128(0)) {
|
|
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
|
}
|
|
|
|
// Cap to available j balance: if approximated output exceeds q_j, it's likely approximation break;
|
|
// fall back to the exact solver to handle capping/edge cases.
|
|
int128 qj64 = s.qInternal[j];
|
|
if (approxOut >= qj64) {
|
|
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
|
}
|
|
|
|
// Everything looks fine; return approximated amountOut and used amountIn (a)
|
|
amountIn = a;
|
|
amountOut = approxOut;
|
|
|
|
// Final guard: ensure output is sensible and not NaN-like (rely on positivity checks above)
|
|
if (amountOut < int128(0)) {
|
|
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
|
}
|
|
|
|
return (amountIn, amountOut);
|
|
}
|
|
|
|
}
|