poolPrice() bugfix; burn() and mint() precision bugfixes
This commit is contained in:
@@ -32,11 +32,11 @@
|
|||||||
| BNB | `0xB8c77482e45F1F44dE1745F52C74426C631bDD52` | nonstandard API |
|
| BNB | `0xB8c77482e45F1F44dE1745F52C74426C631bDD52` | nonstandard API |
|
||||||
|
|
||||||
|
|
||||||
# Proof of Concept Pool
|
# OG Pool
|
||||||
|
|
||||||
```
|
```
|
||||||
Name: Liquidity Party POC
|
Name: Original Genesis of Liquidity Party
|
||||||
Symbol: POC.LP
|
Symbol: OG.LP
|
||||||
Kappa: 0.01
|
Kappa: 0.01
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -52,3 +52,14 @@ Symbol: POC.LP
|
|||||||
| UNI | 0.00145 | `0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984` |
|
| UNI | 0.00145 | `0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984` |
|
||||||
| PEPE | 0.00215 | |
|
| PEPE | 0.00215 | |
|
||||||
| SHIB | 0.00215 | |
|
| SHIB | 0.00215 | |
|
||||||
|
|
||||||
|
USDT 0xdAC17F958D2ee523a2206206994597C13D831ec7
|
||||||
|
USDC 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
|
||||||
|
WBTC 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599
|
||||||
|
WETH 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
|
||||||
|
UNI 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984
|
||||||
|
WSOL 0xD31a59c85aE9D8edEFeC411D448f90841571b89c
|
||||||
|
TRX 0x50327c6c5a14DCaDE707ABad2E27eB517df87AB5
|
||||||
|
AAVE 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9
|
||||||
|
PEPE 0x6982508145454Ce325dDbE47a25d4ec3d2311933
|
||||||
|
SHIB 0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE
|
||||||
@@ -776,19 +776,19 @@ library LMSRStabilized {
|
|||||||
return _exp(qInternal[baseTokenIndex].sub(qInternal[quoteTokenIndex]).mul(invB));
|
return _exp(qInternal[baseTokenIndex].sub(qInternal[quoteTokenIndex]).mul(invB));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Price of one unit of the LP size-metric (S = sum q_i) denominated in `quote` asset (Q64.64)
|
/// @notice Total pool value denominated in `quote` asset (Q64.64, internal quote units)
|
||||||
/// @dev Computes: poolPrice_quote = (1 / S) * sum_j q_j * exp((q_j - q_quote) / b)
|
/// @dev Computes: poolValue_quote = sum_j q_j * exp((q_j - q_quote) / b)
|
||||||
function poolPrice(State storage s, uint256 quoteTokenIndex) internal view returns (int128) {
|
function poolValue(State storage s, uint256 quoteTokenIndex) internal view returns (int128) {
|
||||||
return poolPrice(s.kappa, s.qInternal, quoteTokenIndex);
|
return poolValue(s.kappa, s.qInternal, quoteTokenIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Pure version: Price of one unit of the LP size-metric (S = sum q_i) denominated in `quote` asset (Q64.64)
|
/// @notice Pure version: Total pool value denominated in `quote` asset (Q64.64, internal quote units)
|
||||||
/// @dev Computes: poolPrice_quote = (1 / S) * sum_j q_j * exp((q_j - q_quote) / b)
|
/// @dev Computes: poolValue_quote = sum_j q_j * exp((q_j - q_quote) / b)
|
||||||
/// @param kappa Liquidity parameter κ (64.64 fixed point)
|
/// @param kappa Liquidity parameter κ (64.64 fixed point)
|
||||||
/// @param qInternal Cached internal balances in 64.64 fixed-point format
|
/// @param qInternal Cached internal balances in 64.64 fixed-point format
|
||||||
/// @param quoteTokenIndex Index of quote token
|
/// @param quoteTokenIndex Index of quote token
|
||||||
/// @return Pool price in 64.64 fixed-point format
|
/// @return Total pool value in 64.64 fixed-point format (internal quote units)
|
||||||
function poolPrice(int128 kappa, int128[] memory qInternal, uint256 quoteTokenIndex) internal pure returns (int128) {
|
function poolValue(int128 kappa, int128[] memory qInternal, uint256 quoteTokenIndex) internal pure returns (int128) {
|
||||||
// Compute b and ensure positivity
|
// Compute b and ensure positivity
|
||||||
int128 sizeMetric = _computeSizeMetric(qInternal);
|
int128 sizeMetric = _computeSizeMetric(qInternal);
|
||||||
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
||||||
@@ -814,8 +814,7 @@ library LMSRStabilized {
|
|||||||
unchecked { j++; }
|
unchecked { j++; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// pool price in units of quote = (1 / S) * acc
|
return acc;
|
||||||
return acc.div(S);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --------------------
|
/* --------------------
|
||||||
|
|||||||
@@ -67,8 +67,12 @@ contract PartyInfo is PartyPoolHelpers, IPartyInfo {
|
|||||||
require(nAssets > 0, "poolPrice: uninit");
|
require(nAssets > 0, "poolPrice: uninit");
|
||||||
require(quoteTokenIndex < nAssets, "poolPrice: idx");
|
require(quoteTokenIndex < nAssets, "poolPrice: idx");
|
||||||
|
|
||||||
// price per unit of qTotal (Q64.64) from LMSR
|
// LMSR total value of pool in terms of quote token
|
||||||
return LMSRStabilized.poolPrice( pool.kappa(), lmsr.qInternal, quoteTokenIndex);
|
int128 value = LMSRStabilized.poolValue(pool.kappa(), lmsr.qInternal, quoteTokenIndex);
|
||||||
|
|
||||||
|
uint256 qd = pool.denominators()[quoteTokenIndex];
|
||||||
|
uint256 supply = pool.totalSupply();
|
||||||
|
return value.mul(ABDKMath64x64.divu(qd * 10**18, supply));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -224,13 +224,13 @@ contract PartyPoolMintImpl is PartyPoolBase {
|
|||||||
return depositAmounts; // Return zeros, initial deposit handled differently
|
return depositAmounts; // Return zeros, initial deposit handled differently
|
||||||
}
|
}
|
||||||
|
|
||||||
// lpTokenAmount / totalLpSupply = depositAmount / currentBalance
|
// Compute mint ratio in Q64.64: ratio = lpTokenAmount / totalSupply
|
||||||
// Therefore: depositAmount = (lpTokenAmount * currentBalance) / totalLpSupply
|
int128 ratio = ABDKMath64x64.divu(lpTokenAmount, totalSupply);
|
||||||
// We round up to protect the pool
|
|
||||||
|
// depositAmount_i = ceil(ratio * currentBalance_i)
|
||||||
for (uint i = 0; i < numAssets; i++) {
|
for (uint i = 0; i < numAssets; i++) {
|
||||||
uint256 currentBalance = cachedUintBalances[i];
|
uint256 currentBalance = cachedUintBalances[i];
|
||||||
// Calculate with rounding up: (a * b + c - 1) / c
|
depositAmounts[i] = _internalToUintCeilPure(ratio, currentBalance);
|
||||||
depositAmounts[i] = (lpTokenAmount * currentBalance + totalSupply - 1) / totalSupply;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return depositAmounts;
|
return depositAmounts;
|
||||||
@@ -247,10 +247,10 @@ contract PartyPoolMintImpl is PartyPoolBase {
|
|||||||
return withdrawAmounts; // Return zeros, nothing to withdraw
|
return withdrawAmounts; // Return zeros, nothing to withdraw
|
||||||
}
|
}
|
||||||
|
|
||||||
// withdrawAmount = floor(lpTokenAmount * currentBalance / totalLpSupply)
|
int128 ratio = ABDKMath64x64.divu(lpTokenAmount, totalSupply);
|
||||||
for (uint i = 0; i < numAssets; i++) {
|
for (uint i = 0; i < numAssets; i++) {
|
||||||
uint256 currentBalance = cachedUintBalances[i];
|
uint256 currentBalance = cachedUintBalances[i];
|
||||||
withdrawAmounts[i] = (lpTokenAmount * currentBalance) / totalSupply;
|
withdrawAmounts[i] = ratio.mulu(currentBalance);
|
||||||
}
|
}
|
||||||
|
|
||||||
return withdrawAmounts;
|
return withdrawAmounts;
|
||||||
@@ -536,7 +536,6 @@ contract PartyPoolMintImpl is PartyPoolBase {
|
|||||||
IERC20 outputToken = _tokens[outputTokenIndex];
|
IERC20 outputToken = _tokens[outputTokenIndex];
|
||||||
_sendTokenTo(outputToken, receiver, amountOut, unwrap);
|
_sendTokenTo(outputToken, receiver, amountOut, unwrap);
|
||||||
|
|
||||||
|
|
||||||
// Update cached balances using computed payout and protocol fee; no on-chain reads
|
// Update cached balances using computed payout and protocol fee; no on-chain reads
|
||||||
|
|
||||||
int128[] memory newQInternal = new int128[](n);
|
int128[] memory newQInternal = new int128[](n);
|
||||||
@@ -574,15 +573,24 @@ contract PartyPoolMintImpl is PartyPoolBase {
|
|||||||
/// @notice Pure version of _internalToUintCeil for use in view functions
|
/// @notice Pure version of _internalToUintCeil for use in view functions
|
||||||
function _internalToUintCeilPure(int128 amount, uint256 base) internal pure returns (uint256) {
|
function _internalToUintCeilPure(int128 amount, uint256 base) internal pure returns (uint256) {
|
||||||
// Convert Q64.64 to uint with ceiling: ceil(amount * base)
|
// Convert Q64.64 to uint with ceiling: ceil(amount * base)
|
||||||
// Use mulu which floors, then add remainder check for ceiling
|
// Fast path: compute floor using mulu, then detect fractional remainder via low 64-bit check
|
||||||
uint256 floored = ABDKMath64x64.mulu(amount, base);
|
uint256 floored = ABDKMath64x64.mulu(amount, base);
|
||||||
// Check if there's a fractional part by computing amount * base - floored
|
|
||||||
int128 baseQ64 = ABDKMath64x64.fromUInt(base);
|
// Extract fractional 64 bits of `amount`; if zero, product is already an integer after scaling
|
||||||
int128 flooredQ64 = ABDKMath64x64.fromUInt(floored);
|
uint64 frac = uint64(uint128(amount));
|
||||||
int128 product = amount.mul(baseQ64);
|
if (frac == 0) {
|
||||||
if (product > flooredQ64) {
|
return floored;
|
||||||
return floored + 1; // Ceiling
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unchecked {
|
||||||
|
// Remainder exists iff (frac * (base mod 2^64)) mod 2^64 != 0
|
||||||
|
uint64 baseL = uint64(base);
|
||||||
|
uint128 low = uint128(frac) * uint128(baseL);
|
||||||
|
if (uint64(low) != 0) {
|
||||||
|
return floored + 1; // Ceiling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return floored;
|
return floored;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
pragma solidity ^0.8.20;
|
pragma solidity ^0.8.20;
|
||||||
|
|
||||||
import "forge-std/Test.sol";
|
import "forge-std/Test.sol";
|
||||||
import "forge-std/console2.sol";
|
|
||||||
import "@openzeppelin/contracts/interfaces/IERC20Metadata.sol";
|
import "@openzeppelin/contracts/interfaces/IERC20Metadata.sol";
|
||||||
import "../src/LMSRStabilized.sol";
|
import "../src/LMSRStabilized.sol";
|
||||||
import "../src/LMSRStabilizedBalancedPair.sol";
|
import "../src/LMSRStabilizedBalancedPair.sol";
|
||||||
@@ -90,7 +89,6 @@ contract LMSRStabilizedTest is Test {
|
|||||||
int128 newE0 = eValues[0].mul(_exp(tradeAmount.div(b)));
|
int128 newE0 = eValues[0].mul(_exp(tradeAmount.div(b)));
|
||||||
int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1]));
|
int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1]));
|
||||||
int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
|
int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
|
||||||
console2.log('slippage', slippage);
|
|
||||||
|
|
||||||
// Slippage should be close to stdSlippage (within 1% relative error)
|
// Slippage should be close to stdSlippage (within 1% relative error)
|
||||||
int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
|
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 newE0 = eValues[0].mul(_exp(tradeAmount.div(b)));
|
||||||
int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1]));
|
int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1]));
|
||||||
int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
|
int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
|
||||||
console2.log('slippage', slippage);
|
|
||||||
int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
|
int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
|
||||||
assertLt(relativeError, ABDKMath64x64.divu(1, 100), "Almost balanced pool slippage error too high");
|
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 newE0 = eValues[0].mul(_exp(tradeAmount.div(b)));
|
||||||
int128 slippageRatio = newE0.div(eValues[0]).div(eValues[2].div(eValues[2]));
|
int128 slippageRatio = newE0.div(eValues[0]).div(eValues[2].div(eValues[2]));
|
||||||
int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
|
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
|
// 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%
|
// 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
|
// When we configure kappa, it is a best case slippage (worst case AMM loss) that only occurs with balanced
|
||||||
// assets
|
// assets
|
||||||
int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
|
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");
|
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 newE0 = eValues[0].mul(_exp(tradeAmount.div(newB)));
|
||||||
int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1]));
|
int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1]));
|
||||||
int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
|
int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
|
||||||
console2.log('post-deposit slippage', slippage);
|
|
||||||
|
|
||||||
int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
|
int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
|
||||||
assertLt(relativeError, ABDKMath64x64.divu(1, 100), "Slippage target not met after deposit");
|
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 newE0 = eValues[0].mul(_exp(tradeAmount.div(newB)));
|
||||||
int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1]));
|
int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1]));
|
||||||
int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
|
int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
|
||||||
console2.log('post-withdrawal slippage', slippage);
|
|
||||||
|
|
||||||
int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
|
int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
|
||||||
assertLt(relativeError, ABDKMath64x64.divu(1, 100), "Slippage target not met after withdrawal");
|
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,
|
// The path independence property isn't perfect due to discrete swap mechanics,
|
||||||
// but the difference should be within reasonable bounds
|
// 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
|
// Basic verification that both paths produce positive outputs
|
||||||
assertTrue(directAmountOut > 0, "Direct swap should produce positive output");
|
assertTrue(directAmountOut > 0, "Direct swap should produce positive output");
|
||||||
@@ -736,8 +725,6 @@ contract LMSRStabilizedTest is Test {
|
|||||||
// Update the state's cached qInternal
|
// Update the state's cached qInternal
|
||||||
_updateCachedQInternal(initialQ);
|
_updateCachedQInternal(initialQ);
|
||||||
|
|
||||||
console2.log("Testing round-trip trades for balanced pool");
|
|
||||||
|
|
||||||
// Use standard trade size
|
// Use standard trade size
|
||||||
int128 tradeAmount = s.qInternal[0].mul(stdTradeSize);
|
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
|
// Calculate round-trip slippage: (initial amount - final amount) / initial amount
|
||||||
int128 roundTripSlippage = (amountIn1.sub(amountOut2)).div(amountIn1);
|
int128 roundTripSlippage = (amountIn1.sub(amountOut2)).div(amountIn1);
|
||||||
|
|
||||||
console2.log("Round-trip slippage (micro-units):");
|
|
||||||
console2.logInt(_toMicro(roundTripSlippage));
|
|
||||||
|
|
||||||
// Verify round-trip slippage is reasonable
|
// Verify round-trip slippage is reasonable
|
||||||
int128 tolerance = ABDKMath64x64.divu(1, 100000); // 0.001% tolerance
|
int128 tolerance = ABDKMath64x64.divu(1, 100000); // 0.001% tolerance
|
||||||
assertLt(roundTripSlippage.abs(), tolerance, "Round-trip slippage should be near zero");
|
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
|
// Second direction: asset 1 -> asset 0
|
||||||
(int128 amountIn1to0, int128 amountOut1to0) = s.swapAmountsForExactInput(1, 0, tradeAmount1, 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
|
// For balanced pools, the swap ratios should be approximately symmetric
|
||||||
int128 ratio0to1 = amountOut0to1.div(amountIn0to1);
|
int128 ratio0to1 = amountOut0to1.div(amountIn0to1);
|
||||||
int128 ratio1to0 = amountOut1to0.div(amountIn1to0);
|
int128 ratio1to0 = amountOut1to0.div(amountIn1to0);
|
||||||
@@ -814,9 +789,6 @@ contract LMSRStabilizedTest is Test {
|
|||||||
int128 ratioDifference = (ratio0to1.sub(ratio1to0)).abs();
|
int128 ratioDifference = (ratio0to1.sub(ratio1to0)).abs();
|
||||||
int128 relativeRatioDiff = ratioDifference.div(ratio0to1.add(ratio1to0).div(ABDKMath64x64.fromInt(2)));
|
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
|
// Assert that the relative difference between ratios is small
|
||||||
int128 tolerance = ABDKMath64x64.divu(5, 100); // 5% tolerance
|
int128 tolerance = ABDKMath64x64.divu(5, 100); // 5% tolerance
|
||||||
assertLt(relativeRatioDiff, tolerance,
|
assertLt(relativeRatioDiff, tolerance,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
/* solhint-disable */
|
/* solhint-disable */
|
||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
import "forge-std/console2.sol";
|
|
||||||
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
|
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
|
||||||
import {CommonBase} from "../lib/forge-std/src/Base.sol";
|
import {CommonBase} from "../lib/forge-std/src/Base.sol";
|
||||||
import {StdAssertions} from "../lib/forge-std/src/StdAssertions.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
|
/// does not undercharge (no value extraction). This test verifies the request succeeds
|
||||||
/// and that computed deposits are at least the proportional floor (ceil >= floor).
|
/// and that computed deposits are at least the proportional floor (ceil >= floor).
|
||||||
function testProportionalMintOneWeiSucceedsAndProtectsPool() public {
|
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);
|
vm.startPrank(alice);
|
||||||
token0.approve(address(pool), type(uint256).max);
|
token0.approve(address(pool), type(uint256).max);
|
||||||
token1.approve(address(pool), type(uint256).max);
|
token1.approve(address(pool), type(uint256).max);
|
||||||
token2.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)
|
// 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
|
// Basic sanity: deposits array length must match token count and not all zero necessarily
|
||||||
assertEq(deposits.length, 3);
|
assertEq(deposits.length, 3);
|
||||||
@@ -270,16 +270,16 @@ contract PartyPoolTest is Test {
|
|||||||
uint256 totalLp = pool.totalSupply();
|
uint256 totalLp = pool.totalSupply();
|
||||||
for (uint i = 0; i < deposits.length; i++) {
|
for (uint i = 0; i < deposits.length; i++) {
|
||||||
uint256 bal = IERC20(pool.allTokens()[i]).balanceOf(address(pool));
|
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)
|
// Ceil (deposit) must be >= floor (pool protected)
|
||||||
assertTrue(deposits[i] >= floorProportional, "deposit must not be less than floor proportion");
|
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)
|
// 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
|
// 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();
|
vm.stopPrank();
|
||||||
}
|
}
|
||||||
@@ -301,9 +301,10 @@ contract PartyPoolTest is Test {
|
|||||||
poolValueBefore += IERC20(toks[i]).balanceOf(address(pool));
|
poolValueBefore += IERC20(toks[i]).balanceOf(address(pool));
|
||||||
}
|
}
|
||||||
uint256 totalLpBefore = pool.totalSupply();
|
uint256 totalLpBefore = pool.totalSupply();
|
||||||
|
uint256 lpAmount = totalLpBefore/10**18; // tiny amount
|
||||||
|
|
||||||
// Compute required deposits and perform mint for 1 wei
|
// 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
|
// Sum deposits as deposited_value
|
||||||
uint256 depositedValue = 0;
|
uint256 depositedValue = 0;
|
||||||
@@ -311,8 +312,8 @@ contract PartyPoolTest is Test {
|
|||||||
depositedValue += deposits[i];
|
depositedValue += deposits[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute mint; it may revert if actualLpToMint == 0 but for 1 wei we expect it to succeed per design.
|
// Execute mint; it may revert if actualLpToMint == 0 but for small nonzero values we expect it to succeed per design.
|
||||||
pool.mint(alice, alice, 1, 0);
|
pool.mint(alice, alice, lpAmount, 0);
|
||||||
|
|
||||||
// Observe minted LP
|
// Observe minted LP
|
||||||
uint256 totalLpAfter = pool.totalSupply();
|
uint256 totalLpAfter = pool.totalSupply();
|
||||||
@@ -321,6 +322,7 @@ contract PartyPoolTest is Test {
|
|||||||
require(minted > 0, "sanity: minted should be > 0 for this test");
|
require(minted > 0, "sanity: minted should be > 0 for this test");
|
||||||
|
|
||||||
// Economic invariant check:
|
// 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
|
// depositedValue / minted >= poolValueBefore / totalLpBefore
|
||||||
// Rearranged (to avoid fractional math): depositedValue * totalLpBefore >= poolValueBefore * minted
|
// 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).
|
// 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
|
// Expected price is 1.0 in ABDK 64.64 fixed point
|
||||||
int128 expected = ABDKMath64x64.fromInt(1);
|
int128 expected = ABDKMath64x64.fromInt(1);
|
||||||
|
|
||||||
// Cast int128 to uint128 then to uint256 for assertEq (values are non-negative)
|
// Allow a small tolerance for fixed-point rounding (~1e-9)
|
||||||
assertEq(uint256(uint128(price)), uint256(uint128(expected)), "Initial pool price must be 1.0000000");
|
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
|
// Mint a small amount of LP into the pool from alice and verify price remains 1.0
|
||||||
vm.startPrank(alice);
|
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)
|
// Re-query the pool price and ensure it remains 1.0 (within exact fixed-point equality)
|
||||||
int128 priceAfter = info.poolPrice(pool, 0);
|
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
|
/// @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
|
// Compute swap-implied price as Q64.64 (quote per base) = amountOut / netIn
|
||||||
int128 swapPrice = ABDKMath64x64.divu(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
|
// Absolute difference between info.price and swap-implied price
|
||||||
int128 slippage = ABDKMath64x64.fromUInt(1) - swapPrice.div(infoPrice);
|
int128 slippage = ABDKMath64x64.fromUInt(1) - swapPrice.div(infoPrice);
|
||||||
console2.log('slippage', slippage);
|
|
||||||
|
|
||||||
// Tolerance ~ 4e-5 in Q64.64
|
// Tolerance ~ 4e-5 in Q64.64
|
||||||
int128 tol = ABDKMath64x64.divu(4, 100_000);
|
int128 tol = ABDKMath64x64.divu(4, 100_000);
|
||||||
|
|||||||
Reference in New Issue
Block a user