This commit is contained in:
tim
2025-10-24 20:01:24 -04:00
parent 2972152e58
commit 96dc134769
11 changed files with 421 additions and 253 deletions

View File

@@ -30,7 +30,8 @@ contract LMSRStabilizedTest is Test {
q[0] = ABDKMath64x64.fromUInt(1_000_000);
q[1] = ABDKMath64x64.fromUInt(1_000_000);
q[2] = ABDKMath64x64.fromUInt(1_000_000);
s.init(q, stdTradeSize, stdSlippage);
int128 b = _computeBFromSlippage(3, q, stdTradeSize, stdSlippage);
s.init(q, b);
}
function initAlmostBalanced() internal {
@@ -38,7 +39,8 @@ contract LMSRStabilizedTest is Test {
q[0] = ABDKMath64x64.fromUInt(999_999);
q[1] = ABDKMath64x64.fromUInt(1_000_000);
q[2] = ABDKMath64x64.fromUInt(1_000_001);
s.init(q, stdTradeSize, stdSlippage);
int128 b = _computeBFromSlippage(3, q, stdTradeSize, stdSlippage);
s.init(q, b);
}
function initImbalanced() internal {
@@ -47,7 +49,8 @@ contract LMSRStabilizedTest is Test {
q[1] = ABDKMath64x64.fromUInt(1e9);
q[2] = ABDKMath64x64.fromUInt(1);
q[3] = ABDKMath64x64.divu(1, 1e9);
s.init(q, stdTradeSize, stdSlippage);
int128 b = _computeBFromSlippage(4, q, stdTradeSize, stdSlippage);
s.init(q, b);
}
@@ -193,7 +196,6 @@ contract LMSRStabilizedTest is Test {
// Verify basic state is still functional
assertTrue(s.nAssets > 0, "State should still be initialized");
assertTrue(s.kappa > int128(0), "Kappa should still be positive");
}
function testRescalingAfterDeposit() public {
@@ -211,7 +213,6 @@ contract LMSRStabilizedTest is Test {
// Store initial parameters
int128 initialB = _computeB(initialQ);
int128 initialKappa = s.kappa;
// Simulate a deposit by increasing all asset quantities by 50%
int128[] memory newQ = new int128[](s.nAssets);
@@ -223,18 +224,15 @@ contract LMSRStabilizedTest is Test {
// Apply the update for proportional change
s.updateForProportionalChange(newQ);
// Verify that b has been rescaled proportionally
// Verify that b remains constant after proportional deposit (fixed-b model)
int128 newB = _computeB(s.qInternal);
int128 expectedRatio = ABDKMath64x64.fromUInt(3).div(ABDKMath64x64.fromUInt(2)); // 1.5x
int128 expectedRatio = ABDKMath64x64.fromInt(1); // invariant b
int128 actualRatio = newB.div(initialB);
int128 tolerance = ABDKMath64x64.divu(1, 1000); // 0.1% tolerance
assertTrue((actualRatio.sub(expectedRatio)).abs() < tolerance, "b did not scale proportionally after deposit");
assertTrue((actualRatio.sub(expectedRatio)).abs() < tolerance, "b should remain constant after deposit");
// Verify kappa remained unchanged
assertTrue((s.kappa.sub(initialKappa)).abs() < tolerance, "kappa should not change after deposit");
// Verify slippage target is still met by performing a trade
// Perform a trade and verify outputs are reasonable
int128 tradeAmount = s.qInternal[0].mul(stdTradeSize);
(int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(0, 1, tradeAmount, 0);
@@ -250,8 +248,10 @@ contract LMSRStabilizedTest is Test {
int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
console2.log('post-deposit slippage', slippage);
int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
assertLt(relativeError, ABDKMath64x64.divu(1, 100), "Slippage target not met after deposit");
// With fixed b, theoretical slippage is exp(a/b) - 1
int128 expectedSlippage = _exp(tradeAmount.div(newB)).sub(ABDKMath64x64.fromInt(1));
int128 slippageError = (slippage.sub(expectedSlippage)).abs();
assertLt(slippageError, ABDKMath64x64.divu(1, 1_000_000), "Observed slippage deviates from model");
}
/// @notice Test balanced2 handling of limitPrice that causes truncation of input a
@@ -260,7 +260,8 @@ contract LMSRStabilizedTest is Test {
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 bInit = _computeBFromSlippage(2, q, stdTradeSize, stdSlippage);
s.init(q, bInit);
// Compute b for constructing meaningful a and limits
int128 b = _computeB(q);
@@ -296,7 +297,8 @@ contract LMSRStabilizedTest is Test {
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 bInit = _computeBFromSlippage(2, q, stdTradeSize, stdSlippage);
s.init(q, bInit);
// Small input a
int128 a = q[0].mul(ABDKMath64x64.divu(1, 1000)); // 0.1% of asset
@@ -326,7 +328,8 @@ contract LMSRStabilizedTest is Test {
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 bInit = _computeBFromSlippage(2, q, stdTradeSize, stdSlippage);
s.init(q, bInit);
int128 limitPrice = ABDKMath64x64.fromInt(1); // equal to current price
@@ -359,7 +362,6 @@ contract LMSRStabilizedTest is Test {
// Store initial parameters
int128 initialB = _computeB(initialQ);
int128 initialKappa = s.kappa;
// Simulate a withdrawal by decreasing all asset quantities by 30%
int128[] memory newQ = new int128[](s.nAssets);
@@ -371,18 +373,15 @@ contract LMSRStabilizedTest is Test {
// Apply the update for proportional change
s.updateForProportionalChange(newQ);
// Verify that b has been rescaled proportionally
// Verify that b remains constant after proportional withdrawal (fixed-b model)
int128 newB = _computeB(s.qInternal);
int128 expectedRatio = ABDKMath64x64.fromUInt(7).div(ABDKMath64x64.fromUInt(10)); // 0.7x
int128 expectedRatio = ABDKMath64x64.fromInt(1); // invariant b
int128 actualRatio = newB.div(initialB);
int128 tolerance = ABDKMath64x64.divu(1, 1000); // 0.1% tolerance
assertTrue((actualRatio.sub(expectedRatio)).abs() < tolerance, "b did not scale proportionally after withdrawal");
assertTrue((actualRatio.sub(expectedRatio)).abs() < tolerance, "b should remain constant after withdrawal");
// Verify kappa remained unchanged
assertTrue((s.kappa.sub(initialKappa)).abs() < tolerance, "kappa should not change after withdrawal");
// Verify slippage target is still met by performing a trade
// Perform a trade and verify outputs are reasonable
int128 tradeAmount = s.qInternal[0].mul(stdTradeSize);
(int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(0, 1, tradeAmount, 0);
@@ -398,8 +397,10 @@ contract LMSRStabilizedTest is Test {
int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
console2.log('post-withdrawal slippage', slippage);
int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
assertLt(relativeError, ABDKMath64x64.divu(1, 100), "Slippage target not met after withdrawal");
// With fixed b, theoretical slippage is exp(a/b) - 1
int128 expectedSlippage = _exp(tradeAmount.div(newB)).sub(ABDKMath64x64.fromInt(1));
int128 slippageError = (slippage.sub(expectedSlippage)).abs();
assertLt(slippageError, ABDKMath64x64.divu(1, 1_000_000), "Observed slippage deviates from model");
}
// --- tests probing numerical stability and boundary conditions ---
@@ -431,8 +432,8 @@ contract LMSRStabilizedTest is Test {
this.externalSwapAmountsForExactInput(0, 1, tradeAmount, ABDKMath64x64.fromInt(1));
}
/// @notice If e_j == 0 we should revert early to avoid div-by-zero
function testEJZeroReverts() public {
/// @notice If q_j == 0, kernel should still handle computation without revert (wrapper enforces caps)
function testZeroQuantityOutputAssetDoesNotRevert() public {
initBalanced();
// Create mock qInternal where asset 1 has zero quantity
@@ -446,8 +447,10 @@ contract LMSRStabilizedTest is Test {
int128 tradeAmount = mockQInternal[0].mul(stdTradeSize);
vm.expectRevert(bytes("LMSR: e_j==0"));
this.externalSwapAmountsForExactInput(0, 1, tradeAmount, 0);
// Should not revert; exact-input uses full input and returns a defined output
(int128 usedIn, int128 outAmt) = this.externalSwapAmountsForExactInput(0, 1, tradeAmount, 0);
assertEq(usedIn, tradeAmount, "exact-input should consume full input without limit");
assertTrue(outAmt >= 0, "output amount should be non-negative when q_j == 0");
}
/// @notice swapAmountsForPriceLimit returns zero if limit equals current price
@@ -534,18 +537,16 @@ contract LMSRStabilizedTest is Test {
this.externalSwapAmountsForExactInput(0, 1, a, 0);
}
// Helper function to compute b from qInternal (either from provided array or state)
// Helper function to fetch fixed b (independent of qInternal)
function _computeB(int128[] memory qInternal) internal view returns (int128) {
int128 sizeMetric = _computeSizeMetric(qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
return s.kappa.mul(sizeMetric);
// silence unused warning for qInternal in tests
qInternal;
return s.bFixed;
}
// Overload that uses state's cached qInternal
// Overload that uses state's fixed b
function _computeB() internal view returns (int128) {
int128 sizeMetric = _computeSizeMetric(s.qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
return s.kappa.mul(sizeMetric);
return s.bFixed;
}
// Helper function to compute size metric (sum of all asset quantities)
@@ -558,6 +559,41 @@ contract LMSRStabilizedTest is Test {
return total;
}
// Local helper: compute fixed b from a target slippage profile.
// For a trade of fraction f of S with target slippage s across n assets:
// E = (1 - s*(n-1)) / (1 + s)
// y = -ln(E) / f
// b = S / y
function _computeBFromSlippage(
uint256 nAssets,
int128[] memory qInternal,
int128 tradeFrac,
int128 targetSlippage
) internal pure returns (int128) {
require(nAssets > 1, "test: n>1");
int128 S = _computeSizeMetric(qInternal);
require(S > int128(0), "test: S<=0");
int128 f = tradeFrac;
require(f > int128(0) && f < ABDKMath64x64.fromInt(1), "test: f out of range");
int128 one = ABDKMath64x64.fromInt(1);
int128 nMinus1 = ABDKMath64x64.fromUInt(nAssets - 1);
// E must be in (0,1)
int128 numerator = one.sub(targetSlippage.mul(nMinus1)); // 1 - s*(n-1)
int128 denominator = one.add(targetSlippage); // 1 + s
require(numerator > int128(0), "test: bad slippage");
int128 E = numerator.div(denominator);
require(E > int128(0) && E < one, "test: E out of range");
int128 y = ABDKMath64x64.ln(E).neg().div(f);
require(y > int128(0), "test: y<=0");
return S.div(y);
}
// Helper function to update the state's cached qInternal
function _updateCachedQInternal(int128[] memory mockQInternal) internal {
// First ensure qInternal array exists with the right size
@@ -955,7 +991,8 @@ contract LMSRStabilizedTest is Test {
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 bInit = _computeBFromSlippage(2, q, stdTradeSize, stdSlippage);
s.init(q, bInit);
// Small trade (well within u <= 0.5 and delta <= 1%)
int128 a = q[0].mul(ABDKMath64x64.divu(1, 1000)); // 0.1% of asset
@@ -986,7 +1023,8 @@ contract LMSRStabilizedTest is Test {
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 bInit = _computeBFromSlippage(2, q, stdTradeSize, stdSlippage);
s.init(q, bInit);
// Prepare newQ starting from equal quantities; we'll grow q0 until delta > DELTA_MAX
int128[] memory newQ = new int128[](2);
@@ -1045,7 +1083,8 @@ contract LMSRStabilizedTest is Test {
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 bInit = _computeBFromSlippage(2, q, stdTradeSize, stdSlippage);
s.init(q, bInit);
// Compute b
int128 b = _computeB(q);