poolPrice() bugfix; burn() and mint() precision bugfixes

This commit is contained in:
tim
2025-12-01 15:42:12 -04:00
parent ea54059337
commit 4e56f54f27
6 changed files with 120 additions and 73 deletions

View File

@@ -2,7 +2,6 @@
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";
@@ -90,7 +89,6 @@ contract LMSRStabilizedTest is Test {
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);
@@ -133,7 +131,6 @@ contract LMSRStabilizedTest is Test {
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");
}
@@ -175,14 +172,12 @@ contract LMSRStabilizedTest is Test {
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");
}
@@ -249,7 +244,6 @@ contract LMSRStabilizedTest is Test {
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");
@@ -398,7 +392,6 @@ contract LMSRStabilizedTest is Test {
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");
@@ -712,10 +705,6 @@ contract LMSRStabilizedTest is Test {
// 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");
@@ -736,8 +725,6 @@ contract LMSRStabilizedTest is Test {
// 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);
@@ -754,9 +741,6 @@ contract LMSRStabilizedTest is Test {
// 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");
@@ -797,15 +781,6 @@ contract LMSRStabilizedTest is Test {
// 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);
@@ -814,9 +789,6 @@ contract LMSRStabilizedTest is Test {
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,

View File

@@ -2,7 +2,6 @@
/* solhint-disable */
pragma solidity ^0.8.30;
import "forge-std/console2.sol";
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
import {CommonBase} from "../lib/forge-std/src/Base.sol";
import {StdAssertions} from "../lib/forge-std/src/StdAssertions.sol";
@@ -254,14 +253,15 @@ contract PartyPoolTest is Test {
/// does not undercharge (no value extraction). This test verifies the request succeeds
/// and that computed deposits are at least the proportional floor (ceil >= floor).
function testProportionalMintOneWeiSucceedsAndProtectsPool() public {
// Request a tiny LP amount (1 wei). Approve pool to transfer _tokens on alice's behalf.
// Request a tiny LP amount. Approve pool to transfer _tokens on alice's behalf.
vm.startPrank(alice);
token0.approve(address(pool), type(uint256).max);
token1.approve(address(pool), type(uint256).max);
token2.approve(address(pool), type(uint256).max);
// Inspect the deposit amounts that the pool will require (these are rounded up)
uint256[] memory deposits = info.mintAmounts(pool, 1);
uint256 lpAmount = pool.totalSupply() / 2**64 + 1; // smallest mintable amount
uint256[] memory deposits = info.mintAmounts(pool, lpAmount);
// Basic sanity: deposits array length must match token count and not all zero necessarily
assertEq(deposits.length, 3);
@@ -270,16 +270,16 @@ contract PartyPoolTest is Test {
uint256 totalLp = pool.totalSupply();
for (uint i = 0; i < deposits.length; i++) {
uint256 bal = IERC20(pool.allTokens()[i]).balanceOf(address(pool));
uint256 floorProportional = (1 * bal) / totalLp; // floor
uint256 floorProportional = (lpAmount * bal) / totalLp; // floor
// Ceil (deposit) must be >= floor (pool protected)
assertTrue(deposits[i] >= floorProportional, "deposit must not be less than floor proportion");
}
// Perform the mint — it should succeed for a 1 wei request (pool uses ceil to protect itself)
pool.mint(alice, alice, 1, 0);
pool.mint(alice, alice, lpAmount, 0);
// After mint, alice should have received at least 1 wei of LP
assertTrue(pool.balanceOf(alice) >= 1, "Alice should receive at least 1 wei LP");
assertTrue(pool.balanceOf(alice) >= lpAmount, "Alice should receive more LP token");
vm.stopPrank();
}
@@ -301,9 +301,10 @@ contract PartyPoolTest is Test {
poolValueBefore += IERC20(toks[i]).balanceOf(address(pool));
}
uint256 totalLpBefore = pool.totalSupply();
uint256 lpAmount = totalLpBefore/10**18; // tiny amount
// Compute required deposits and perform mint for 1 wei
uint256[] memory deposits = info.mintAmounts(pool, 1);
uint256[] memory deposits = info.mintAmounts(pool, lpAmount);
// Sum deposits as deposited_value
uint256 depositedValue = 0;
@@ -311,8 +312,8 @@ contract PartyPoolTest is Test {
depositedValue += deposits[i];
}
// Execute mint; it may revert if actualLpToMint == 0 but for 1 wei we expect it to succeed per design.
pool.mint(alice, alice, 1, 0);
// Execute mint; it may revert if actualLpToMint == 0 but for small nonzero values we expect it to succeed per design.
pool.mint(alice, alice, lpAmount, 0);
// Observe minted LP
uint256 totalLpAfter = pool.totalSupply();
@@ -321,6 +322,7 @@ contract PartyPoolTest is Test {
require(minted > 0, "sanity: minted should be > 0 for this test");
// Economic invariant check:
// The depositor should pay at least as much value per LP token as the pool's rate before the mint:
// depositedValue / minted >= poolValueBefore / totalLpBefore
// Rearranged (to avoid fractional math): depositedValue * totalLpBefore >= poolValueBefore * minted
// Use >= to allow the pool to charge equal-or-more value per LP (protects against extraction).
@@ -1079,8 +1081,12 @@ contract PartyPoolTest is Test {
// Expected price is 1.0 in ABDK 64.64 fixed point
int128 expected = ABDKMath64x64.fromInt(1);
// Cast int128 to uint128 then to uint256 for assertEq (values are non-negative)
assertEq(uint256(uint128(price)), uint256(uint128(expected)), "Initial pool price must be 1.0000000");
// Allow a small tolerance for fixed-point rounding (~1e-9)
int128 ratio = ABDKMath64x64.div(price, expected);
int128 expectedRatio = ABDKMath64x64.fromUInt(1);
int128 tol = ABDKMath64x64.divu(1, 1_000_000_000);
int128 diff = ratio.sub(expectedRatio).abs();
assertLe(diff, tol, "poolPrice(token0) should be ~ 1.000000000");
// Mint a small amount of LP into the pool from alice and verify price remains 1.0
vm.startPrank(alice);
@@ -1105,7 +1111,57 @@ contract PartyPoolTest is Test {
// Re-query the pool price and ensure it remains 1.0 (within exact fixed-point equality)
int128 priceAfter = info.poolPrice(pool, 0);
assertEq(uint256(uint128(priceAfter)), uint256(uint128(expected)), "Pool price should remain 1.0000000 after mint");
// Allow a small tolerance for fixed-point rounding (~1e-9)
ratio = ABDKMath64x64.div(price, priceAfter);
expectedRatio = ABDKMath64x64.fromUInt(1);
tol = ABDKMath64x64.divu(1, 1_000_000_000);
diff = ratio.sub(expectedRatio).abs();
assertLe(diff, tol, "Pool price should remain 1.0000000 after mint");
}
/// @notice For the same 3x-imbalanced pool, verify that the LP pool price in terms of
/// token0 is 1/3 of the pool price in terms of token1 (up to rounding).
function testPoolPriceWhenToken0HasThreeTimesToken1() public {
// Build tokens array (reuse test tokens)
IERC20[] memory tokens = new IERC20[](3);
tokens[0] = IERC20(address(token0));
tokens[1] = IERC20(address(token1));
tokens[2] = IERC20(address(token2));
uint256 feePpm = 1000;
int128 kappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
// Same 3x imbalance as in testPriceWhenToken0HasThreeTimesToken1
uint256[] memory deposits = new uint256[](3);
deposits[0] = INIT_BAL * 3; // token0 = 3 * INIT_BAL
deposits[1] = INIT_BAL; // token1 = INIT_BAL
deposits[2] = INIT_BAL * 2; // token2 = 2 * INIT_BAL
(IPartyPool poolCustom, ) = Deploy.newPartyPoolWithDeposits(
"LP3X_POOLPRICE",
"LP3X_POOLPRICE",
tokens,
kappa,
feePpm,
feePpm,
false,
deposits,
INIT_BAL * 6 * 10**18
);
// Get LP price in terms of token0 and token1 (Q64.64, quote units per LP)
int128 p0 = info.poolPrice(poolCustom, 0); // token0 as quote
int128 p1 = info.poolPrice(poolCustom, 1); // token1 as quote
// ratio = p0 / p1 should be close to 3
int128 ratio = ABDKMath64x64.div(p0, p1);
int128 expectedRatio = ABDKMath64x64.fromUInt(3);
// Allow a small tolerance for fixed-point rounding (~1e-6)
int128 tol = ABDKMath64x64.divu(1, 1_000_000);
int128 diff = ratio.sub(expectedRatio).abs();
assertLe(diff, tol, "poolPrice(token0) should be ~ 1/3 of poolPrice(token1)");
}
/// @notice Create a 3-token pool where token0 has 3x the balance of token1 and verify
@@ -1183,12 +1239,9 @@ contract PartyPoolTest is Test {
// Compute swap-implied price as Q64.64 (quote per base) = amountOut / netIn
int128 swapPrice = ABDKMath64x64.divu(amountOut, netIn);
console2.log('info price', infoPrice);
console2.log('swap price', swapPrice);
// Absolute difference between info.price and swap-implied price
int128 slippage = ABDKMath64x64.fromUInt(1) - swapPrice.div(infoPrice);
console2.log('slippage', slippage);
// Tolerance ~ 4e-5 in Q64.64
int128 tol = ABDKMath64x64.divu(4, 100_000);