balanced pair optimization
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user