dxod repo init
This commit is contained in:
24
test/Counter.t.sol
Normal file
24
test/Counter.t.sol
Normal file
@@ -0,0 +1,24 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
import {Counter} from "../src/Counter.sol";
|
||||
|
||||
contract CounterTest is Test {
|
||||
Counter public counter;
|
||||
|
||||
function setUp() public {
|
||||
counter = new Counter();
|
||||
counter.setNumber(0);
|
||||
}
|
||||
|
||||
function test_Increment() public {
|
||||
counter.increment();
|
||||
assertEq(counter.number(), 1);
|
||||
}
|
||||
|
||||
function testFuzz_SetNumber(uint256 x) public {
|
||||
counter.setNumber(x);
|
||||
assertEq(counter.number(), x);
|
||||
}
|
||||
}
|
||||
861
test/LMSRStabilized.t.sol
Normal file
861
test/LMSRStabilized.t.sol
Normal file
@@ -0,0 +1,861 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "forge-std/Test.sol";
|
||||
import "forge-std/console.sol";
|
||||
import "@openzeppelin/contracts/interfaces/IERC20Metadata.sol";
|
||||
import "../src/LMSRStabilized.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.nAssets > 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;
|
||||
|
||||
// Simulate a deposit by increasing all asset quantities by 50%
|
||||
int128[] memory newQ = new int128[](s.nAssets);
|
||||
for (uint i = 0; i < s.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");
|
||||
}
|
||||
|
||||
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%
|
||||
int128[] memory newQ = new int128[](s.nAssets);
|
||||
for (uint i = 0; i < s.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.nAssets > 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 If e_j == 0 we should revert early to avoid div-by-zero
|
||||
function testEJZeroReverts() public {
|
||||
initBalanced();
|
||||
|
||||
// Create mock qInternal where asset 1 has zero quantity
|
||||
int128[] memory mockQInternal = new int128[](3);
|
||||
mockQInternal[0] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
mockQInternal[1] = int128(0); // Zero quantity for asset 1
|
||||
mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
|
||||
// Update the state's cached qInternal
|
||||
_updateCachedQInternal(mockQInternal);
|
||||
|
||||
int128 tradeAmount = mockQInternal[0].mul(stdTradeSize);
|
||||
|
||||
vm.expectRevert(bytes("LMSR: e_j==0"));
|
||||
this.externalSwapAmountsForExactInput(0, 1, tradeAmount, 0);
|
||||
}
|
||||
|
||||
/// @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
|
||||
int128[] memory initialQValues = new int128[](s.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[](s.nAssets);
|
||||
for (uint i = 0; i < s.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
|
||||
int128[] memory backupQ = new int128[](s.nAssets);
|
||||
for (uint i = 0; i < s.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 payout, int128 burned) = 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 payout, int128 burned) = 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");
|
||||
}
|
||||
|
||||
}
|
||||
14
test/MockERC20.sol
Normal file
14
test/MockERC20.sol
Normal file
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
|
||||
contract MockERC20 is ERC20 {
|
||||
uint8 private immutable _decimals;
|
||||
|
||||
constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) {_decimals = decimals_;}
|
||||
|
||||
function decimals() public view virtual override returns (uint8) {return _decimals;}
|
||||
function mint(address account, uint256 amount) external {_mint(account, amount);}
|
||||
function burn(address account, uint256 amount) external {_burn(account, amount);}
|
||||
}
|
||||
1423
test/PartyPool.t.sol
Normal file
1423
test/PartyPool.t.sol
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user