balanced pair optimization

This commit is contained in:
tim
2025-09-18 22:16:01 -04:00
parent a96b494cef
commit 3eba6412a6
7 changed files with 549 additions and 27 deletions

View File

@@ -5,6 +5,7 @@ import "forge-std/Test.sol";
import "forge-std/console.sol";
import "@openzeppelin/contracts/interfaces/IERC20Metadata.sol";
import "../src/LMSRStabilized.sol";
import "../src/LMSRStabilizedBalancedPair.sol";
/// @notice Forge tests for LMSRStabilized
@@ -253,6 +254,96 @@ contract LMSRStabilizedTest is Test {
assertLt(relativeError, ABDKMath64x64.divu(1, 100), "Slippage target not met after deposit");
}
/// @notice Test balanced2 handling of limitPrice that causes truncation of input a
function testBalanced2LimitTruncation() public {
// Two-asset balanced pool
int128[] memory q = new int128[](2);
q[0] = ABDKMath64x64.fromUInt(1_000_000);
q[1] = ABDKMath64x64.fromUInt(1_000_000);
s.init(q, stdTradeSize, stdSlippage);
// Compute b for constructing meaningful a and limits
int128 b = _computeB(q);
// Choose a large requested input so that the limitPrice will truncate it.
int128 aRequested = b.mul(ABDKMath64x64.fromUInt(10)); // a/b = 10 (within EXP_LIMIT)
// Small limit slightly above current price (r0 == 1). Use 0.1% above parity.
int128 limitPrice = ABDKMath64x64.fromInt(1).add(ABDKMath64x64.divu(1, 1000)); // 1.001
// Call optimized balanced2 and exact versions
(int128 inApprox, int128 outApprox) = LMSRStabilizedBalancedPair.swapAmountsForExactInput(s, 0, 1, aRequested, limitPrice);
(int128 inExact, int128 outExact) = s.swapAmountsForExactInput(0, 1, aRequested, limitPrice);
// Ensure exact returned something sensible
assertTrue(outExact > 0, "exact output should be positive");
// Relative error tolerance 0.001% (1e-5)
int128 relErr = (outApprox.sub(outExact)).abs().div(outExact);
int128 tol = ABDKMath64x64.divu(1, 100_000);
assertLt(relErr, tol, "balanced2 truncated output deviates from exact beyond tolerance");
// Input used should be close as well
int128 inRelErr = (inApprox.sub(inExact)).abs();
// If exact truncated, inExact likely equals aLimit computed by ln; allow small absolute difference tolerance of 1e-6 relative to b
int128 absTol = b.div(ABDKMath64x64.fromUInt(1_000_000)); // b * 1e-6
assertTrue(inRelErr <= absTol, "balanced2 truncated input differs from exact beyond small absolute tolerance");
}
/// @notice Test balanced2 with a limitPrice that does not truncate the provided input
function testBalanced2LimitNoTruncation() public {
// Two-asset balanced pool
int128[] memory q = new int128[](2);
q[0] = ABDKMath64x64.fromUInt(1_000_000);
q[1] = ABDKMath64x64.fromUInt(1_000_000);
s.init(q, stdTradeSize, stdSlippage);
// Small input a
int128 a = q[0].mul(ABDKMath64x64.divu(1, 1000)); // 0.1% of asset
// Very relaxed limit (2x current price) which should not truncate
int128 limitPrice = ABDKMath64x64.fromUInt(2);
(int128 inApprox, int128 outApprox) = LMSRStabilizedBalancedPair.swapAmountsForExactInput(s, 0, 1, a, limitPrice);
(int128 inExact, int128 outExact) = s.swapAmountsForExactInput(0, 1, a, limitPrice);
// Exact outputs must be positive
assertTrue(outExact > 0, "exact output should be positive");
// Expect almost exact match when no truncation occurs; use tight tolerance
int128 relErr = (outApprox.sub(outExact)).abs().div(outExact);
int128 tol = ABDKMath64x64.divu(1, 100_000); // 0.001%
assertLt(relErr, tol, "balanced2 no-truncate output deviates from exact beyond tolerance");
// AmountIn should equal provided a for both functions
assertEq(inApprox, a, "balanced2 should use full input when not truncated");
assertEq(inExact, a, "exact should use full input when not truncated");
}
/// @notice Test that balanced2 reverts when limitPrice <= current price (no partial fill allowed)
function testBalanced2LimitRevertWhenAtOrBelowCurrent() public {
// Two-asset balanced pool
int128[] memory q = new int128[](2);
q[0] = ABDKMath64x64.fromUInt(1_000_000);
q[1] = ABDKMath64x64.fromUInt(1_000_000);
s.init(q, stdTradeSize, stdSlippage);
int128 limitPrice = ABDKMath64x64.fromInt(1); // equal to current price
vm.expectRevert(bytes("LMSR: limitPrice <= current price"));
this._swapAmountsForExactInput_balanced2(0, 1, q[0].mul(ABDKMath64x64.divu(1, 1000)), limitPrice);
}
function _swapAmountsForExactInput_balanced2(
uint256 i,
uint256 j,
int128 a,
int128 limitPrice
) external view returns (int128 amountIn, int128 amountOut) {
return LMSRStabilizedBalancedPair.swapAmountsForExactInput(s, i,j,a,limitPrice);
}
function testRescalingAfterWithdrawal() public {
// Initialize pool with almost balanced assets
initAlmostBalanced();
@@ -858,4 +949,116 @@ contract LMSRStabilizedTest is Test {
assertTrue(payout > 0, "payout must be positive even when one asset is zero");
}
/// @notice Test that the balanced2 polynomial approximation is accurate for a two-asset balanced pool
function testBalanced2ApproxAccuracy() public {
// Create a minimal two-asset balanced pool
int128[] memory q = new int128[](2);
q[0] = ABDKMath64x64.fromUInt(1_000_000);
q[1] = ABDKMath64x64.fromUInt(1_000_000);
s.init(q, stdTradeSize, stdSlippage);
// Small trade (well within u <= 0.5 and delta <= 1%)
int128 a = q[0].mul(ABDKMath64x64.divu(1, 1000)); // 0.1% of asset
// Compute approx and exact
(int128 inApprox, int128 outApprox) = LMSRStabilizedBalancedPair.swapAmountsForExactInput(s, 0, 1, a, 0);
(int128 inExact, int128 outExact) = s.swapAmountsForExactInput(0, 1, a, 0);
// Sanity
assertTrue(outExact > 0, "Exact output should be positive");
// Relative error: |approx - exact| / exact
int128 relErr = (outApprox.sub(outExact)).abs().div(outExact);
// Require relative error < 0.001% (1e-5) => expressed as 1 / 100_000
int128 tolerance = ABDKMath64x64.divu(1, 100_000);
assertLt(relErr, tolerance, "balanced2 approximation relative error too large");
// AmountIn should equal requested a (no truncation)
assertEq(inApprox, a, "balanced2 approximation should use full input when no limitPrice");
assertEq(inExact, a, "exact computation should use full input when no limitPrice");
}
/// @notice Test that when the parity assumption is violated, the balanced2 helper falls back
/// to the exact implementation (we expect identical outputs).
function testBalanced2FallbackWhenParityViolated() public {
// Start with two-asset balanced pool (we'll mutate it)
int128[] memory q = new int128[](2);
q[0] = ABDKMath64x64.fromUInt(1_000_000);
q[1] = ABDKMath64x64.fromUInt(1_000_000);
s.init(q, stdTradeSize, stdSlippage);
// Prepare newQ starting from equal quantities; we'll grow q0 until delta > DELTA_MAX
int128[] memory newQ = new int128[](2);
newQ[0] = q[0];
newQ[1] = q[1];
// DELTA_MAX used by the library: 0.01
int128 DELTA_MAX = ABDKMath64x64.divu(1, 100);
// Iteratively increase q0 until the library's delta = (q0 - q1) / b > DELTA_MAX
// We cap iterations to avoid infinite loops in pathological cases.
bool reached = false;
for (uint iter = 0; iter < 64; iter++) {
// Update the state's cached qInternal with the candidate imbalance
_updateCachedQInternal(newQ);
// Compute the current b and delta using the state's parameters
int128 bNow = _computeB(); // uses s.qInternal and s.kappa
// avoid division by zero
if (bNow == int128(0)) { break; }
int128 deltaNow = newQ[0].sub(newQ[1]).div(bNow);
if (deltaNow < int128(0)) { deltaNow = deltaNow.neg(); }
if (deltaNow > DELTA_MAX) {
reached = true;
break;
}
// Increase q0 by 10% each iteration to quickly cross the threshold
newQ[0] = newQ[0].mul(ABDKMath64x64.fromUInt(11)).div(ABDKMath64x64.fromUInt(10));
}
// Ensure we actually achieved the desired imbalance for a meaningful test
_updateCachedQInternal(newQ);
int128 finalB = _computeB();
int128 finalDelta = newQ[0].sub(newQ[1]).div(finalB);
if (finalDelta < int128(0)) finalDelta = finalDelta.neg();
assertTrue(finalDelta > DELTA_MAX, "failed to create delta > DELTA_MAX in test");
// Small trade amount
int128 a = newQ[0].mul(ABDKMath64x64.divu(1, 1000));
// Call both functions; balanced2 should detect parity violation and fall back to exact
(int128 inApprox, int128 outApprox) = LMSRStabilizedBalancedPair.swapAmountsForExactInput(s, 0, 1, a, 0);
(int128 inExact, int128 outExact) = s.swapAmountsForExactInput(0, 1, a, 0);
// Because parity assumption is violated balanced2 should fall back to exact implementation
assertEq(inApprox, inExact, "fallback should return identical amountIn");
assertEq(outApprox, outExact, "fallback should return identical amountOut");
}
/// @notice Test that the balanced2 helper falls back when scaled input u = a/b is too large
function testBalanced2FallbackOnLargeInput() public {
// Two-asset balanced pool
int128[] memory q = new int128[](2);
q[0] = ABDKMath64x64.fromUInt(1_000_000);
q[1] = ABDKMath64x64.fromUInt(1_000_000);
s.init(q, stdTradeSize, stdSlippage);
// Compute b
int128 b = _computeB(q);
// Choose a so that u = a / b = 0.75 (> 0.5 U_MAX)
int128 a = b.mul(ABDKMath64x64.divu(3, 4)); // a/b = 0.75
// Call both functions and expect fallback (identical results)
(int128 inApprox, int128 outApprox) = LMSRStabilizedBalancedPair.swapAmountsForExactInput(s, 0, 1, a, 0);
(int128 inExact, int128 outExact) = s.swapAmountsForExactInput(0, 1, a, 0);
assertEq(inApprox, inExact, "fallback on large input should return identical amountIn");
assertEq(outApprox, outExact, "fallback on large input should return identical amountOut");
}
}