1050 lines
46 KiB
Solidity
1050 lines
46 KiB
Solidity
// SPDX-License-Identifier: UNLICENSED
|
|
pragma solidity ^0.8.20;
|
|
|
|
import "forge-std/Test.sol";
|
|
import "forge-std/console2.sol";
|
|
import "@openzeppelin/contracts/interfaces/IERC20Metadata.sol";
|
|
import "../src/LMSRStabilized.sol";
|
|
import "../src/LMSRStabilizedBalancedPair.sol";
|
|
|
|
|
|
/// @notice Forge tests for LMSRStabilized
|
|
contract LMSRStabilizedTest is Test {
|
|
using LMSRStabilized for LMSRStabilized.State;
|
|
using ABDKMath64x64 for int128;
|
|
|
|
LMSRStabilized.State internal s;
|
|
|
|
int128 stdTradeSize;
|
|
int128 stdSlippage;
|
|
|
|
|
|
function setUp() public {
|
|
// 0.10% slippage when taking 1.00% of the assets
|
|
stdTradeSize = ABDKMath64x64.divu(100,10_000);
|
|
stdSlippage = ABDKMath64x64.divu(10,10_000);
|
|
}
|
|
|
|
function initBalanced() internal {
|
|
int128[] memory q = new int128[](3);
|
|
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);
|
|
}
|
|
|
|
function initAlmostBalanced() internal {
|
|
int128[] memory q = new int128[](3);
|
|
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);
|
|
}
|
|
|
|
function initImbalanced() internal {
|
|
int128[] memory q = new int128[](4);
|
|
q[0] = ABDKMath64x64.fromUInt(1);
|
|
q[1] = ABDKMath64x64.fromUInt(1e9);
|
|
q[2] = ABDKMath64x64.fromUInt(1);
|
|
q[3] = ABDKMath64x64.divu(1, 1e9);
|
|
s.init(q, stdTradeSize, stdSlippage);
|
|
}
|
|
|
|
|
|
function testInitBalanced() public {
|
|
// Test 1: Balanced Pool Initialization
|
|
initBalanced();
|
|
|
|
// Create mock qInternal for testing
|
|
int128[] memory mockQInternal = new int128[](3);
|
|
mockQInternal[0] = ABDKMath64x64.fromUInt(1_000_000);
|
|
mockQInternal[1] = ABDKMath64x64.fromUInt(1_000_000);
|
|
mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_000);
|
|
|
|
// Update the state's cached qInternal
|
|
_updateCachedQInternal(mockQInternal);
|
|
|
|
// Verify slippage by performing asset swaps and checking price impact
|
|
int128 tradeAmount = mockQInternal[0].mul(stdTradeSize);
|
|
|
|
// For a balanced pool, test asset 0 -> asset 1 swap
|
|
(int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(0, 1, tradeAmount, 0);
|
|
|
|
// Verify amountIn and amountOut are reasonable
|
|
assertTrue(amountIn > 0, "amountIn should be positive");
|
|
assertTrue(amountOut > 0, "amountOut should be positive");
|
|
|
|
// Calculate slippage = (initialPrice/finalPrice - 1)
|
|
// Compute e values dynamically for price ratio
|
|
int128 b = _computeB(mockQInternal);
|
|
int128[] memory eValues = _computeE(b, mockQInternal);
|
|
|
|
// For balanced pool, initial price ratio is 1:1
|
|
int128 initialRatio = eValues[0].div(eValues[1]);
|
|
|
|
// Verify initial ratio for balanced pool is approximately 1:1
|
|
assertTrue((initialRatio.sub(ABDKMath64x64.fromInt(1))).abs() < ABDKMath64x64.divu(1, 10000),
|
|
"Initial price ratio should be close to 1:1");
|
|
|
|
// After trade, the new e values would be different
|
|
int128 newE0 = eValues[0].mul(_exp(tradeAmount.div(b)));
|
|
int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1]));
|
|
int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
|
|
console2.log('slippage', slippage);
|
|
|
|
// Slippage should be close to stdSlippage (within 1% relative error)
|
|
int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
|
|
assertLt(relativeError, ABDKMath64x64.divu(1, 100), "Balanced pool slippage error too high");
|
|
}
|
|
|
|
function testInitAlmostBalanced() public {
|
|
// Test 2: Almost Balanced Pool Initialization
|
|
initAlmostBalanced();
|
|
|
|
// Create mock qInternal for testing
|
|
int128[] memory mockQInternal = new int128[](3);
|
|
mockQInternal[0] = ABDKMath64x64.fromUInt(999_999);
|
|
mockQInternal[1] = ABDKMath64x64.fromUInt(1_000_000);
|
|
mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_001);
|
|
|
|
// Update the state's cached qInternal
|
|
_updateCachedQInternal(mockQInternal);
|
|
|
|
// Verify slippage for almost balanced pool
|
|
int128 tradeAmount = mockQInternal[0].mul(stdTradeSize);
|
|
|
|
(int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(0, 1, tradeAmount, 0);
|
|
|
|
// Verify amountIn and amountOut are reasonable
|
|
assertTrue(amountIn > 0, "amountIn should be positive");
|
|
assertTrue(amountOut > 0, "amountOut should be positive");
|
|
|
|
// Compute e values dynamically for price ratio
|
|
int128 b = _computeB(mockQInternal);
|
|
int128[] memory eValues = _computeE(b, mockQInternal);
|
|
int128 initialRatio = eValues[0].div(eValues[1]);
|
|
int128 relDiff = (initialRatio.sub(ABDKMath64x64.fromInt(1))).abs();
|
|
// Verify the initial ratio is close to but not exactly 1:1
|
|
assertTrue(relDiff < ABDKMath64x64.divu(1, 1000),
|
|
"Initial ratio should be close to 1:1 for almost balanced pool");
|
|
assertTrue(relDiff > ABDKMath64x64.divu(1, 10000000),
|
|
"Initial ratio should not be exactly 1:1 for almost balanced pool");
|
|
|
|
int128 newE0 = eValues[0].mul(_exp(tradeAmount.div(b)));
|
|
int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1]));
|
|
int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
|
|
console2.log('slippage', slippage);
|
|
int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
|
|
assertLt(relativeError, ABDKMath64x64.divu(1, 100), "Almost balanced pool slippage error too high");
|
|
}
|
|
|
|
function testInitImbalanced() public {
|
|
// Test 3: Imbalanced Pool Initialization
|
|
initImbalanced();
|
|
|
|
// Create mock qInternal for testing
|
|
int128[] memory mockQInternal = new int128[](4);
|
|
mockQInternal[0] = ABDKMath64x64.fromUInt(1);
|
|
mockQInternal[1] = ABDKMath64x64.fromUInt(1e9);
|
|
mockQInternal[2] = ABDKMath64x64.fromUInt(1);
|
|
mockQInternal[3] = ABDKMath64x64.divu(1, 1e9);
|
|
|
|
// Update the state's cached qInternal
|
|
_updateCachedQInternal(mockQInternal);
|
|
|
|
// For imbalanced pool, we need to try an "average" swap
|
|
// We'll use asset 0 -> asset 2 as it's more balanced than asset 0 -> asset 1
|
|
int128 tradeAmount = mockQInternal[0].mul(stdTradeSize);
|
|
|
|
// Compute e values dynamically for price ratio
|
|
int128 b = _computeB(mockQInternal);
|
|
int128[] memory eValues = _computeE(b, mockQInternal);
|
|
|
|
// Verify the ratios between small and large assets is different
|
|
int128 initialRatio = eValues[0].div(eValues[3]); // Assets 0 and 2 match, and assets 1 and 3 match. 0 and 3 differ.
|
|
int128 relDiff = (initialRatio.sub(ABDKMath64x64.fromInt(1))).abs();
|
|
// Verify initial ratio shows significant imbalance
|
|
assertTrue(relDiff != 0, "Initial ratio should show imbalance");
|
|
|
|
(int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(0, 2, tradeAmount, 0);
|
|
|
|
// Verify amountIn and amountOut are reasonable
|
|
assertTrue(amountIn > 0, "amountIn should be positive");
|
|
assertTrue(amountOut > 0, "amountOut should be positive");
|
|
|
|
int128 newE0 = eValues[0].mul(_exp(tradeAmount.div(b)));
|
|
int128 slippageRatio = newE0.div(eValues[0]).div(eValues[2].div(eValues[2]));
|
|
int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
|
|
console2.log('slippage', slippage);
|
|
|
|
// Since the imbalance is extreme, with one coin worth lots more than the others, the actual slippage for
|
|
// this swap is actually off by about 100%
|
|
// When we configure kappa, it is a best case slippage (worst case AMM loss) that only occurs with balanced
|
|
// assets
|
|
int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
|
|
console2.log('relative error', relativeError);
|
|
assertLt(relativeError, ABDKMath64x64.divu(100, 100), "Imbalanced pool slippage error too high");
|
|
}
|
|
|
|
function testRecentering() public {
|
|
// Recentering functionality has been removed since we no longer cache intermediate values
|
|
// This test is now a no-op but kept for API compatibility
|
|
initAlmostBalanced();
|
|
|
|
// Verify basic state is still functional
|
|
assertTrue(s.qInternal.length > 0, "State should still be initialized");
|
|
assertTrue(s.kappa > int128(0), "Kappa should still be positive");
|
|
}
|
|
|
|
function testRescalingAfterDeposit() public {
|
|
// Initialize pool with almost balanced assets
|
|
initAlmostBalanced();
|
|
|
|
// Create initial asset quantities
|
|
int128[] memory initialQ = new int128[](3);
|
|
initialQ[0] = ABDKMath64x64.fromUInt(999_999);
|
|
initialQ[1] = ABDKMath64x64.fromUInt(1_000_000);
|
|
initialQ[2] = ABDKMath64x64.fromUInt(1_000_001);
|
|
|
|
// Update the state's cached qInternal
|
|
_updateCachedQInternal(initialQ);
|
|
|
|
// Store initial parameters
|
|
int128 initialB = _computeB(initialQ);
|
|
int128 initialKappa = s.kappa;
|
|
|
|
uint256 nAssets = s.qInternal.length;
|
|
// Simulate a deposit by increasing all asset quantities by 50%
|
|
int128[] memory newQ = new int128[](nAssets);
|
|
for (uint i = 0; i < nAssets; i++) {
|
|
// Increase by 50%
|
|
newQ[i] = initialQ[i].mul(ABDKMath64x64.fromUInt(3).div(ABDKMath64x64.fromUInt(2))); // 1.5x
|
|
}
|
|
|
|
// Apply the update for proportional change
|
|
s.updateForProportionalChange(newQ);
|
|
|
|
// Verify that b has been rescaled proportionally
|
|
int128 newB = _computeB(s.qInternal);
|
|
int128 expectedRatio = ABDKMath64x64.fromUInt(3).div(ABDKMath64x64.fromUInt(2)); // 1.5x
|
|
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");
|
|
|
|
// 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
|
|
int128 tradeAmount = s.qInternal[0].mul(stdTradeSize);
|
|
(int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(0, 1, tradeAmount, 0);
|
|
|
|
// Verify computed swap amounts
|
|
assertTrue(amountIn > 0, "Swap amountIn should be positive");
|
|
assertTrue(amountOut > 0, "Swap amountOut should be positive");
|
|
// Verify amountOut is reasonable compared to amountIn (not a severe loss)
|
|
assertTrue(amountOut.div(amountIn) > ABDKMath64x64.divu(9, 10), "Swap should not incur severe loss");
|
|
|
|
int128[] memory eValues = _computeE(newB, s.qInternal);
|
|
int128 newE0 = eValues[0].mul(_exp(tradeAmount.div(newB)));
|
|
int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1]));
|
|
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");
|
|
}
|
|
|
|
/// @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();
|
|
|
|
// Create initial asset quantities
|
|
int128[] memory initialQ = new int128[](3);
|
|
initialQ[0] = ABDKMath64x64.fromUInt(999_999);
|
|
initialQ[1] = ABDKMath64x64.fromUInt(1_000_000);
|
|
initialQ[2] = ABDKMath64x64.fromUInt(1_000_001);
|
|
|
|
// Update the state's cached qInternal
|
|
_updateCachedQInternal(initialQ);
|
|
|
|
// Store initial parameters
|
|
int128 initialB = _computeB(initialQ);
|
|
int128 initialKappa = s.kappa;
|
|
|
|
// Simulate a withdrawal by decreasing all asset quantities by 30%
|
|
uint256 nAssets = s.qInternal.length;
|
|
int128[] memory newQ = new int128[](nAssets);
|
|
for (uint i = 0; i < nAssets; i++) {
|
|
// Decrease by 30%
|
|
newQ[i] = initialQ[i].mul(ABDKMath64x64.fromUInt(7).div(ABDKMath64x64.fromUInt(10))); // 0.7x
|
|
}
|
|
|
|
// Apply the update for proportional change
|
|
s.updateForProportionalChange(newQ);
|
|
|
|
// Verify that b has been rescaled proportionally
|
|
int128 newB = _computeB(s.qInternal);
|
|
int128 expectedRatio = ABDKMath64x64.fromUInt(7).div(ABDKMath64x64.fromUInt(10)); // 0.7x
|
|
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");
|
|
|
|
// 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
|
|
int128 tradeAmount = s.qInternal[0].mul(stdTradeSize);
|
|
(int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(0, 1, tradeAmount, 0);
|
|
|
|
// Verify computed swap amounts
|
|
assertTrue(amountIn > 0, "Swap amountIn should be positive");
|
|
assertTrue(amountOut > 0, "Swap amountOut should be positive");
|
|
// Verify amountOut is reasonable compared to amountIn (not a severe loss)
|
|
assertTrue(amountOut.div(amountIn) > ABDKMath64x64.divu(9, 10), "Swap should not incur severe loss");
|
|
|
|
int128[] memory eValues = _computeE(newB, s.qInternal);
|
|
int128 newE0 = eValues[0].mul(_exp(tradeAmount.div(newB)));
|
|
int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1]));
|
|
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");
|
|
}
|
|
|
|
// --- tests probing numerical stability and boundary conditions ---
|
|
|
|
/// @notice Recentering functionality has been removed - this test is now a no-op
|
|
function testRecenterShiftTooLargeReverts() public {
|
|
initAlmostBalanced();
|
|
// Recentering has been removed, so this test now just verifies basic functionality
|
|
assertTrue(s.qInternal.length > 0, "State should still be initialized");
|
|
}
|
|
|
|
/// @notice limitPrice <= current price should revert (no partial fill)
|
|
function testLimitPriceRevertWhenAtOrBelowCurrent() public {
|
|
initBalanced();
|
|
|
|
// Create mock qInternal for testing
|
|
int128[] memory mockQInternal = new int128[](3);
|
|
mockQInternal[0] = ABDKMath64x64.fromUInt(1_000_000);
|
|
mockQInternal[1] = ABDKMath64x64.fromUInt(1_000_000);
|
|
mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_000);
|
|
|
|
// Update the state's cached qInternal
|
|
_updateCachedQInternal(mockQInternal);
|
|
|
|
// For balanced pool r0 = 1. Use limitPrice == 1 which should revert.
|
|
int128 tradeAmount = mockQInternal[0].mul(stdTradeSize);
|
|
|
|
vm.expectRevert(bytes("LMSR: limitPrice <= current price"));
|
|
this.externalSwapAmountsForExactInput(0, 1, tradeAmount, ABDKMath64x64.fromInt(1));
|
|
}
|
|
|
|
/// @notice swapAmountsForPriceLimit returns zero if limit equals current price
|
|
function testSwapAmountsForPriceLimitZeroWhenLimitEqualsPrice() public {
|
|
initBalanced();
|
|
|
|
// Create mock qInternal for testing
|
|
int128[] memory mockQInternal = new int128[](3);
|
|
mockQInternal[0] = ABDKMath64x64.fromUInt(1_000_000);
|
|
mockQInternal[1] = ABDKMath64x64.fromUInt(1_000_000);
|
|
mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_000);
|
|
|
|
// Update the state's cached qInternal
|
|
_updateCachedQInternal(mockQInternal);
|
|
|
|
// For balanced pool r0 = 1. swapAmountsForPriceLimit with limit==1 should be zero
|
|
vm.expectRevert('LMSR: limitPrice <= current price');
|
|
this.externalSwapAmountsForPriceLimit(0, 1, ABDKMath64x64.fromInt(1));
|
|
|
|
// Try with a limit price slightly above 1, which should not revert
|
|
try this.externalSwapAmountsForPriceLimit(0, 1, ABDKMath64x64.fromInt(1).add(ABDKMath64x64.divu(1, 1000))) returns (int128 _amountIn, int128 _maxOut) {
|
|
// Verify that the returned values are reasonable
|
|
assertTrue(_amountIn > 0, "amountIn should be positive for valid limit price");
|
|
assertTrue(_maxOut > 0, "maxOut should be positive for valid limit price");
|
|
} catch {
|
|
fail("Should not revert with limit price > current price");
|
|
}
|
|
}
|
|
|
|
function externalSwapAmountsForPriceLimit(uint256 i, uint256 j, int128 limitPrice) external view
|
|
returns (int128, int128) {
|
|
return s.swapAmountsForPriceLimit(i, j, limitPrice);
|
|
}
|
|
|
|
/// @notice Gas/throughput test: perform 100 alternating swaps between asset 0 and 1
|
|
function testSwapGas() public {
|
|
// Initialize the almost-balanced pool
|
|
initAlmostBalanced();
|
|
|
|
// Create mock qInternal that we'll update through swaps
|
|
int128[] memory currentQ = new int128[](3);
|
|
currentQ[0] = ABDKMath64x64.fromUInt(999_999);
|
|
currentQ[1] = ABDKMath64x64.fromUInt(1_000_000);
|
|
currentQ[2] = ABDKMath64x64.fromUInt(1_000_001);
|
|
|
|
// Update the state's cached qInternal
|
|
_updateCachedQInternal(currentQ);
|
|
|
|
// Perform 100 swaps, alternating between asset 0 -> 1 and 1 -> 0
|
|
for (uint256 iter = 0; iter < 100; iter++) {
|
|
uint256 from = (iter % 2 == 0) ? 0 : 1;
|
|
uint256 to = (from == 0) ? 1 : 0;
|
|
|
|
// Use standard trade size applied to the 'from' asset's current quantity
|
|
int128 tradeAmount = s.qInternal[from].mul(stdTradeSize);
|
|
|
|
// Compute swap amounts and apply to state
|
|
(int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(from, to, tradeAmount, 0);
|
|
|
|
// applySwap now updates the internal qInternal directly
|
|
s.applySwap(from, to, amountIn, amountOut);
|
|
}
|
|
}
|
|
|
|
/// @notice Extremely large a that makes a/b exceed expLimit should revert
|
|
function testAmountOutABOverflowReverts() public {
|
|
initBalanced();
|
|
|
|
// Create mock qInternal for testing
|
|
int128[] memory mockQInternal = new int128[](3);
|
|
mockQInternal[0] = ABDKMath64x64.fromUInt(1_000_000);
|
|
mockQInternal[1] = ABDKMath64x64.fromUInt(1_000_000);
|
|
mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_000);
|
|
|
|
// Update the state's cached qInternal
|
|
_updateCachedQInternal(mockQInternal);
|
|
|
|
int128 b = _computeB(mockQInternal);
|
|
// Pick a such that a/b = 33 (expLimit is 32). a = b * 33
|
|
int128 aOverB_target = ABDKMath64x64.fromInt(33);
|
|
int128 a = b.mul(aOverB_target);
|
|
|
|
vm.expectRevert(bytes("LMSR: a/b too large (would overflow exp)"));
|
|
this.externalSwapAmountsForExactInput(0, 1, a, 0);
|
|
}
|
|
|
|
// Helper function to compute b from qInternal (either from provided array or state)
|
|
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);
|
|
}
|
|
|
|
// Overload that uses state's cached qInternal
|
|
function _computeB() internal view returns (int128) {
|
|
int128 sizeMetric = _computeSizeMetric(s.qInternal);
|
|
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
|
return s.kappa.mul(sizeMetric);
|
|
}
|
|
|
|
// Helper function to compute size metric (sum of all asset quantities)
|
|
function _computeSizeMetric(int128[] memory qInternal) internal pure returns (int128) {
|
|
int128 total = int128(0);
|
|
for (uint i = 0; i < qInternal.length; ) {
|
|
total = total.add(qInternal[i]);
|
|
unchecked { i++; }
|
|
}
|
|
return total;
|
|
}
|
|
|
|
// Helper function to update the state's cached qInternal
|
|
function _updateCachedQInternal(int128[] memory mockQInternal) internal {
|
|
// First ensure qInternal array exists with the right size
|
|
if (s.qInternal.length != mockQInternal.length) {
|
|
s.qInternal = new int128[](mockQInternal.length);
|
|
}
|
|
|
|
// Copy values from mockQInternal to state's qInternal
|
|
for (uint i = 0; i < mockQInternal.length; ) {
|
|
s.qInternal[i] = mockQInternal[i];
|
|
unchecked { i++; }
|
|
}
|
|
}
|
|
|
|
// Helper function to compute M and Z dynamically
|
|
function _computeMAndZ(int128 b, int128[] memory qInternal) internal pure returns (int128 M, int128 Z) {
|
|
require(qInternal.length > 0, "LMSR: no assets");
|
|
|
|
// Compute y_i = q_i / b for numerical stability
|
|
int128[] memory y = new int128[](qInternal.length);
|
|
for (uint i = 0; i < qInternal.length; ) {
|
|
y[i] = qInternal[i].div(b);
|
|
unchecked { i++; }
|
|
}
|
|
|
|
// Find max y for centering (M = maxY)
|
|
M = y[0];
|
|
for (uint i = 1; i < qInternal.length; ) {
|
|
if (y[i] > M) M = y[i];
|
|
unchecked { i++; }
|
|
}
|
|
|
|
// Compute Z = sum of exp(z_i) where z_i = y_i - M
|
|
Z = int128(0);
|
|
for (uint i = 0; i < qInternal.length; ) {
|
|
int128 z_i = y[i].sub(M);
|
|
int128 e_i = _exp(z_i);
|
|
Z = Z.add(e_i);
|
|
unchecked { i++; }
|
|
}
|
|
}
|
|
|
|
// Helper function to compute all e[i] = exp(z[i]) values dynamically
|
|
function _computeE(int128 b, int128[] memory qInternal) internal pure returns (int128[] memory e) {
|
|
(int128 M, ) = _computeMAndZ(b, qInternal);
|
|
e = new int128[](qInternal.length);
|
|
|
|
for (uint i = 0; i < qInternal.length; ) {
|
|
int128 y_i = qInternal[i].div(b);
|
|
int128 z_i = y_i.sub(M);
|
|
e[i] = _exp(z_i);
|
|
unchecked { i++; }
|
|
}
|
|
}
|
|
|
|
// Helper function to calculate exp (copied from LMSRStabilized library)
|
|
function _exp(int128 x) internal pure returns (int128) {
|
|
return ABDKMath64x64.exp(x);
|
|
}
|
|
|
|
// External helper function that wraps swapAmountsForExactInput to properly handle reverts in tests
|
|
function externalSwapAmountsForExactInput(
|
|
uint i,
|
|
uint j,
|
|
int128 a,
|
|
int128 limitPrice
|
|
) external view returns (int128 amountIn, int128 amountOut) {
|
|
return s.swapAmountsForExactInput(i, j, a, limitPrice);
|
|
}
|
|
|
|
// External helper function that wraps recenterIfNeeded to properly handle reverts in tests
|
|
function externalRecenterIfNeeded() external {
|
|
// Recentering has been removed - this is now a no-op
|
|
}
|
|
|
|
// External helper function that wraps applySwap to properly handle reverts in tests
|
|
function externalApplySwap(
|
|
uint i,
|
|
uint j,
|
|
int128 amountIn,
|
|
int128 amountOut
|
|
) external {
|
|
s.applySwap(i, j, amountIn, amountOut);
|
|
}
|
|
|
|
// Small helper: convert a Q64.64 int128 into micro-units (value * 1e6) as an int256 for readable logging.
|
|
// Example: if x represents 0.001 (Q64.64), _toMicro(x) will return ~1000.
|
|
function _toMicro(int128 x) internal pure returns (int256) {
|
|
int256 ONE = int256(uint256(0x10000000000000000)); // 2^64
|
|
return (int256(x) * 1_000_000) / ONE;
|
|
}
|
|
|
|
/// @notice Test that applySwap correctly validates swap parameters and updates qInternal
|
|
function testApplySwap() public {
|
|
// Initialize with balanced assets
|
|
initBalanced();
|
|
|
|
// Create mock qInternal for testing
|
|
int128[] memory mockQInternal = new int128[](3);
|
|
mockQInternal[0] = ABDKMath64x64.fromUInt(1_000_000);
|
|
mockQInternal[1] = ABDKMath64x64.fromUInt(1_000_000);
|
|
mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_000);
|
|
|
|
// Update the state's cached qInternal
|
|
_updateCachedQInternal(mockQInternal);
|
|
|
|
// Save original values for comparison
|
|
int128 originalQ0 = s.qInternal[0];
|
|
int128 originalQ1 = s.qInternal[1];
|
|
|
|
// Calculate swap amounts from asset 0 to asset 1
|
|
int128 tradeAmount = mockQInternal[0].mul(stdTradeSize);
|
|
|
|
(int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(0, 1, tradeAmount, 0);
|
|
|
|
// Verify basic swap calculation worked
|
|
assertTrue(amountIn > 0, "amountIn should be positive");
|
|
assertTrue(amountOut > 0, "amountOut should be positive");
|
|
|
|
// Apply the swap - should not revert for valid inputs
|
|
s.applySwap(0, 1, amountIn, amountOut);
|
|
|
|
// Verify qInternal is correctly updated
|
|
// Input asset should increase by amountIn
|
|
assertEq(s.qInternal[0], originalQ0.add(amountIn), "qInternal[0] should be updated");
|
|
// Output asset should decrease by amountOut
|
|
assertEq(s.qInternal[1], originalQ1.sub(amountOut), "qInternal[1] should be updated");
|
|
}
|
|
|
|
/// @notice Test path independence by comparing direct vs indirect swaps
|
|
function testPathIndependence() public {
|
|
// Start with a balanced pool
|
|
initBalanced();
|
|
|
|
// Create initial quantities
|
|
uint256 nAssets = s.qInternal.length;
|
|
int128[] memory initialQValues = new int128[](nAssets);
|
|
initialQValues[0] = ABDKMath64x64.fromUInt(1_000_000);
|
|
initialQValues[1] = ABDKMath64x64.fromUInt(1_000_000);
|
|
initialQValues[2] = ABDKMath64x64.fromUInt(1_000_000);
|
|
|
|
// Update the state's cached qInternal
|
|
_updateCachedQInternal(initialQValues);
|
|
|
|
// Test path independence by computing swap outcomes without state changes
|
|
int128 directSwapAmount = initialQValues[0].mul(stdTradeSize);
|
|
|
|
// Store a backup of the original values to restore between swaps
|
|
int128[] memory backupQ = new int128[](nAssets);
|
|
for (uint i = 0; i < nAssets; i++) {
|
|
backupQ[i] = s.qInternal[i];
|
|
}
|
|
|
|
// Path 1: Direct swap from asset 0 to asset 2
|
|
(/*int128 directAmountIn*/, int128 directAmountOut) = s.swapAmountsForExactInput(0, 2, directSwapAmount, 0);
|
|
|
|
// Restore original state for second path
|
|
_updateCachedQInternal(backupQ);
|
|
|
|
// Path 2: Swap from asset 0 to asset 1, then from asset 1 to asset 2
|
|
(int128 indirectAmountIn1, int128 indirectAmountOut1) = s.swapAmountsForExactInput(0, 1, directSwapAmount, 0);
|
|
|
|
// Update state for second leg of indirect path
|
|
s.qInternal[0] = s.qInternal[0].sub(indirectAmountIn1);
|
|
s.qInternal[1] = s.qInternal[1].add(indirectAmountOut1);
|
|
|
|
// Second swap: asset 1 -> asset 2
|
|
(/*int128 indirectAmountIn2*/, int128 indirectAmountOut2) = s.swapAmountsForExactInput(1, 2, indirectAmountOut1, 0);
|
|
|
|
// The path independence property isn't perfect due to discrete swap mechanics,
|
|
// but the difference should be within reasonable bounds
|
|
console2.log("Direct swap output:");
|
|
console2.logInt(directAmountOut);
|
|
console2.log("Indirect swap total output:");
|
|
console2.logInt(indirectAmountOut2);
|
|
|
|
// Basic verification that both paths produce positive outputs
|
|
assertTrue(directAmountOut > 0, "Direct swap should produce positive output");
|
|
assertTrue(indirectAmountOut2 > 0, "Indirect swap should produce positive output");
|
|
}
|
|
|
|
/// @notice Test round-trip trades to verify near-zero slippage
|
|
function testRoundTripTradesAcrossAllPools() public {
|
|
// Test with balanced pool only since we removed state caching
|
|
initBalanced();
|
|
|
|
// Create mock qInternal
|
|
int128[] memory initialQ = new int128[](3);
|
|
initialQ[0] = ABDKMath64x64.fromUInt(1_000_000);
|
|
initialQ[1] = ABDKMath64x64.fromUInt(1_000_000);
|
|
initialQ[2] = ABDKMath64x64.fromUInt(1_000_000);
|
|
|
|
// Update the state's cached qInternal
|
|
_updateCachedQInternal(initialQ);
|
|
|
|
console2.log("Testing round-trip trades for balanced pool");
|
|
|
|
// Use standard trade size
|
|
int128 tradeAmount = s.qInternal[0].mul(stdTradeSize);
|
|
|
|
// Step 1: Swap asset 0 -> asset 1
|
|
(int128 amountIn1, int128 amountOut1) = s.swapAmountsForExactInput(0, 1, tradeAmount, 0);
|
|
|
|
// Update quantities for step 2
|
|
s.qInternal[0] = s.qInternal[0].sub(amountIn1);
|
|
s.qInternal[1] = s.qInternal[1].add(amountOut1);
|
|
|
|
// Step 2: Swap back asset 1 -> asset 0
|
|
(/*int128 amountIn2*/, int128 amountOut2) = s.swapAmountsForExactInput(1, 0, amountOut1, 0);
|
|
|
|
// Calculate round-trip slippage: (initial amount - final amount) / initial amount
|
|
int128 roundTripSlippage = (amountIn1.sub(amountOut2)).div(amountIn1);
|
|
|
|
console2.log("Round-trip slippage (micro-units):");
|
|
console2.logInt(_toMicro(roundTripSlippage));
|
|
|
|
// Verify round-trip slippage is reasonable
|
|
int128 tolerance = ABDKMath64x64.divu(1, 100000); // 0.001% tolerance
|
|
assertLt(roundTripSlippage.abs(), tolerance, "Round-trip slippage should be near zero");
|
|
}
|
|
|
|
/// @notice Test that slippage is approximately equal in both directions for small swaps
|
|
function testBidirectionalSlippageSymmetry() public {
|
|
// Initialize with balanced assets for clearest slippage measurement
|
|
initBalanced();
|
|
|
|
// Create mock qInternal
|
|
int128[] memory initialQ = new int128[](3);
|
|
initialQ[0] = ABDKMath64x64.fromUInt(1_000_000);
|
|
initialQ[1] = ABDKMath64x64.fromUInt(1_000_000);
|
|
initialQ[2] = ABDKMath64x64.fromUInt(1_000_000);
|
|
|
|
// Update the state's cached qInternal
|
|
_updateCachedQInternal(initialQ);
|
|
|
|
// Use small trade size for clear slippage measurement
|
|
int128 tradeSize = ABDKMath64x64.divu(5, 10_000); // 0.05% of pool
|
|
int128 tradeAmount0 = s.qInternal[0].mul(tradeSize);
|
|
int128 tradeAmount1 = s.qInternal[1].mul(tradeSize);
|
|
|
|
// Store original state to restore between tests
|
|
uint256 nAssets = s.qInternal.length;
|
|
int128[] memory backupQ = new int128[](nAssets);
|
|
for (uint i = 0; i < nAssets; i++) {
|
|
backupQ[i] = s.qInternal[i];
|
|
}
|
|
|
|
// First direction: asset 0 -> asset 1
|
|
(int128 amountIn0to1, int128 amountOut0to1) = s.swapAmountsForExactInput(0, 1, tradeAmount0, 0);
|
|
|
|
// Restore original state
|
|
_updateCachedQInternal(backupQ);
|
|
|
|
// Second direction: asset 1 -> asset 0
|
|
(int128 amountIn1to0, int128 amountOut1to0) = s.swapAmountsForExactInput(1, 0, tradeAmount1, 0);
|
|
|
|
console2.log("0->1 swap amountIn:");
|
|
console2.logInt(amountIn0to1);
|
|
console2.log("0->1 swap amountOut:");
|
|
console2.logInt(amountOut0to1);
|
|
console2.log("1->0 swap amountIn:");
|
|
console2.logInt(amountIn1to0);
|
|
console2.log("1->0 swap amountOut:");
|
|
console2.logInt(amountOut1to0);
|
|
|
|
// For balanced pools, the swap ratios should be approximately symmetric
|
|
int128 ratio0to1 = amountOut0to1.div(amountIn0to1);
|
|
int128 ratio1to0 = amountOut1to0.div(amountIn1to0);
|
|
|
|
// Calculate relative difference between the ratios
|
|
int128 ratioDifference = (ratio0to1.sub(ratio1to0)).abs();
|
|
int128 relativeRatioDiff = ratioDifference.div(ratio0to1.add(ratio1to0).div(ABDKMath64x64.fromInt(2)));
|
|
|
|
console2.log("Relative ratio difference (micro-units):");
|
|
console2.logInt(_toMicro(relativeRatioDiff));
|
|
|
|
// Assert that the relative difference between ratios is small
|
|
int128 tolerance = ABDKMath64x64.divu(5, 100); // 5% tolerance
|
|
assertLt(relativeRatioDiff, tolerance,
|
|
"Swap ratios should be approximately equal in both directions");
|
|
}
|
|
|
|
/// @notice Test that basic swap functionality works across multiple operations
|
|
function testZConsistencyAfterMultipleSwaps() public {
|
|
// Initialize with balanced assets
|
|
initBalanced();
|
|
|
|
// Create mock qInternal that we'll update through swaps
|
|
int128[] memory initialQ = new int128[](3);
|
|
initialQ[0] = ABDKMath64x64.fromUInt(1_000_000);
|
|
initialQ[1] = ABDKMath64x64.fromUInt(1_000_000);
|
|
initialQ[2] = ABDKMath64x64.fromUInt(1_000_000);
|
|
|
|
// Update the state's cached qInternal
|
|
_updateCachedQInternal(initialQ);
|
|
|
|
// Perform multiple swaps in different directions
|
|
for (uint i = 0; i < 5; i++) {
|
|
// Swap from asset i%3 to asset (i+1)%3
|
|
uint from = i % 3;
|
|
uint to = (i + 1) % 3;
|
|
|
|
int128 tradeAmount = s.qInternal[from].mul(stdTradeSize);
|
|
|
|
(int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(from, to, tradeAmount, 0);
|
|
|
|
// Apply swap to update internal state
|
|
s.applySwap(from, to, amountIn, amountOut);
|
|
|
|
// Basic validation that swap worked
|
|
assertTrue(amountIn > 0, "amountIn should be positive");
|
|
assertTrue(amountOut > 0, "amountOut should be positive");
|
|
}
|
|
}
|
|
|
|
// --- New tests for single-token mint/burn helpers ---
|
|
|
|
/// @notice Basic sanity check for swapAmountsForMint: small single-token input
|
|
function testSwapAmountsForMintBasic() public {
|
|
initBalanced();
|
|
|
|
// Use a small single-token input (stdTradeSize fraction of asset 0)
|
|
int128 a = s.qInternal[0].mul(stdTradeSize);
|
|
|
|
(int128 consumed, int128 lpIncrease) = s.swapAmountsForMint(0, a);
|
|
|
|
// consumed must be non-negative and <= provided a (partial-fill allowed)
|
|
assertTrue(consumed > 0, "consumed should be positive");
|
|
assertTrue(consumed <= a, "consumed should not exceed provided input");
|
|
|
|
// lpIncrease should be positive
|
|
assertTrue(lpIncrease > 0, "lpIncrease should be positive");
|
|
}
|
|
|
|
/// @notice Large input for swapAmountsForMint should return a valid partial fill (consumed <= provided)
|
|
function testSwapAmountsForMintLargeInputPartial() public {
|
|
initAlmostBalanced();
|
|
|
|
// Provide a large input far above stdTradeSize to exercise cap logic
|
|
int128 a = s.qInternal[0].mul(ABDKMath64x64.fromUInt(1000)); // 1000x one-asset quantity
|
|
|
|
(int128 consumed, int128 lpIncrease) = s.swapAmountsForMint(0, a);
|
|
|
|
// Should not consume more than provided
|
|
assertTrue(consumed <= a, "consumed must be <= provided");
|
|
|
|
// If nothing could be consumed, the helper should revert earlier; otherwise positive
|
|
assertTrue(consumed > 0, "consumed should be positive for large input in normal pools");
|
|
assertTrue(lpIncrease > 0, "lpIncrease should be positive for large input");
|
|
}
|
|
|
|
/// @notice Basic swapAmountsForBurn sanity: small alpha should return positive single-asset payout
|
|
function testSwapAmountsForBurnBasic() public {
|
|
initBalanced();
|
|
|
|
// Burn alpha fraction of pool
|
|
int128 alpha = ABDKMath64x64.divu(1, 100); // 1%
|
|
int128 S = _computeSizeMetric(s.qInternal);
|
|
|
|
(int128 burned, int128 payout) = s.swapAmountsForBurn(0, alpha);
|
|
|
|
// burned should equal alpha * S
|
|
assertEq(burned, alpha.mul(S), "burned size-metric mismatch");
|
|
|
|
// payout should be positive
|
|
assertTrue(payout > 0, "payout must be positive for balanced pool burn");
|
|
}
|
|
|
|
/// @notice If some assets have zero quantity, burn should skip them but still return payout when possible
|
|
function testSwapAmountsForBurnWithZeroAsset() public {
|
|
initBalanced();
|
|
|
|
// Make asset 1 empty; others non-zero
|
|
int128[] memory mockQInternal = new int128[](3);
|
|
mockQInternal[0] = ABDKMath64x64.fromUInt(1_000_000);
|
|
mockQInternal[1] = int128(0); // zero
|
|
mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_000);
|
|
_updateCachedQInternal(mockQInternal);
|
|
|
|
int128 alpha = ABDKMath64x64.divu(1, 100); // 1%
|
|
(int128 burned, int128 payout) = s.swapAmountsForBurn(0, alpha);
|
|
|
|
// Should still burn the size metric
|
|
int128 S = _computeSizeMetric(mockQInternal);
|
|
assertEq(burned, alpha.mul(S), "burned size-metric mismatch with zero asset present");
|
|
|
|
// Payout should be at least the direct redeemed portion (alpha * q_i)
|
|
assertTrue(payout >= alpha.mul(mockQInternal[0]), "payout should be >= direct redeemed portion");
|
|
|
|
// Payout must be positive
|
|
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");
|
|
}
|
|
|
|
}
|