PartyPlanner; chain.json

This commit is contained in:
tim
2025-09-20 16:04:31 -04:00
parent 9fe0179e6a
commit 10d432070d
12 changed files with 1133 additions and 317 deletions

View File

@@ -25,9 +25,12 @@ contract FlashBorrower is IPartyFlashCallback {
address public recipient;
address[] public tokens;
constructor(address _pool, address[] memory _tokens) {
constructor(address _pool, IERC20[] memory _tokens) {
pool = _pool;
tokens = _tokens;
tokens = new address[](_tokens.length);
for (uint i = 0; i < _tokens.length; i++) {
tokens[i] = address(_tokens[i]);
}
}
function setAction(Action _action, address _recipient) external {
@@ -162,7 +165,11 @@ contract GasTest is Test {
// Deploy pool with a small fee to test fee-handling paths (use 1000 ppm = 0.1%)
uint256 feePpm = 1000;
string memory poolName = string(abi.encodePacked("LP", vm.toString(numTokens)));
PartyPool newPool = new PartyPool(poolName, poolName, tokens, bases, tradeFrac, targetSlippage, feePpm, feePpm, false);
IERC20[] memory ierc20Tokens = new IERC20[](tokens.length);
for (uint i = 0; i < tokens.length; i++) {
ierc20Tokens[i] = IERC20(tokens[i]);
}
PartyPool newPool = new PartyPool(poolName, poolName, ierc20Tokens, bases, tradeFrac, targetSlippage, feePpm, feePpm, false);
// Transfer initial deposit amounts into pool before initial mint
for (uint256 i = 0; i < numTokens; i++) {
@@ -170,7 +177,7 @@ contract GasTest is Test {
}
// Perform initial mint (initial deposit); receiver is this contract
newPool.mint(address(0), address(this), 0, 0);
newPool.initialMint(address(this), 0);
return newPool;
}
@@ -197,7 +204,11 @@ contract GasTest is Test {
uint256 feePpm = 1000;
string memory poolName = string(abi.encodePacked("LPs", vm.toString(numTokens)));
// Note the final 'true' arg to activate stable-pair optimization path
PartyPool newPool = new PartyPool(poolName, poolName, tokens, bases, tradeFrac, targetSlippage, feePpm, feePpm, true);
IERC20[] memory ierc20Tokens = new IERC20[](tokens.length);
for (uint i = 0; i < tokens.length; i++) {
ierc20Tokens[i] = IERC20(tokens[i]);
}
PartyPool newPool = new PartyPool(poolName, poolName, ierc20Tokens, bases, tradeFrac, targetSlippage, feePpm, feePpm, true);
// Transfer initial deposit amounts into pool before initial mint
for (uint256 i = 0; i < numTokens; i++) {
@@ -205,7 +216,7 @@ contract GasTest is Test {
}
// Perform initial mint (initial deposit); receiver is this contract
newPool.mint(address(0), address(this), 0, 0);
newPool.initialMint(address(this), 0);
return newPool;
}
@@ -228,7 +239,7 @@ contract GasTest is Test {
/// @notice Setup a flash borrower for testing
function setupFlashBorrower() internal returns (FlashBorrower borrower) {
// Get token addresses from the 2-token pool
address[] memory tokenAddresses = pool2.allTokens();
IERC20[] memory tokenAddresses = pool2.allTokens();
// Deploy the borrower contract
borrower = new FlashBorrower(address(pool2), tokenAddresses);
@@ -236,22 +247,22 @@ contract GasTest is Test {
// Mint tokens to alice to be used for repayments and approve borrower
vm.startPrank(alice);
for (uint256 i = 0; i < tokenAddresses.length; i++) {
TestERC20(tokenAddresses[i]).mint(alice, INIT_BAL * 2);
TestERC20(tokenAddresses[i]).approve(address(borrower), type(uint256).max);
TestERC20(address(tokenAddresses[i])).mint(alice, INIT_BAL * 2);
TestERC20(address(tokenAddresses[i])).approve(address(borrower), type(uint256).max);
}
vm.stopPrank();
}
/// @notice Helper function: perform 10 swaps back-and-forth between the first two tokens.
function _performSwapGasTest(PartyPool testPool) internal {
address[] memory tokens = testPool.allTokens();
IERC20[] memory tokens = testPool.allTokens();
require(tokens.length >= 2, "Pool must have at least 2 tokens");
// Ensure alice approves pool for both tokens
vm.prank(alice);
TestERC20(tokens[0]).approve(address(testPool), type(uint256).max);
TestERC20(address(tokens[0])).approve(address(testPool), type(uint256).max);
vm.prank(alice);
TestERC20(tokens[1]).approve(address(testPool), type(uint256).max);
TestERC20(address(tokens[1])).approve(address(testPool), type(uint256).max);
uint256 maxIn = 1_000;
@@ -310,13 +321,13 @@ contract GasTest is Test {
function _performSwapMintBurnSwapGasTest(PartyPool testPool) internal {
uint256 iterations = 10;
uint256 input = 1_000;
address[] memory tokens = testPool.allTokens();
IERC20[] memory tokens = testPool.allTokens();
// Top up alice so repeated operations won't fail
TestERC20(tokens[0]).mint(alice, iterations * input * 2);
TestERC20(address(tokens[0])).mint(alice, iterations * input * 2);
vm.startPrank(alice);
TestERC20(tokens[0]).approve(address(testPool), type(uint256).max);
TestERC20(address(tokens[0])).approve(address(testPool), type(uint256).max);
for (uint256 k = 0; k < iterations; k++) {
// Mint LP by providing single-token input; receive LP minted
@@ -355,14 +366,14 @@ contract GasTest is Test {
function _performMintBurnGasTest(PartyPool testPool) internal {
uint256 iterations = 50;
uint256 input = 1_000;
address[] memory poolTokens = testPool.allTokens();
IERC20[] memory poolTokens = testPool.allTokens();
vm.startPrank(alice);
// Mint additional tokens to alice and approve pool to transfer tokens for proportional mint
for (uint256 i = 0; i < poolTokens.length; i++) {
TestERC20(poolTokens[i]).mint(alice, iterations * input * 2);
TestERC20(poolTokens[i]).approve(address(testPool), type(uint256).max);
TestERC20(address(poolTokens[i])).mint(alice, iterations * input * 2);
TestERC20(address(poolTokens[i])).approve(address(testPool), type(uint256).max);
}
for (uint256 k = 0; k < iterations; k++) {
@@ -422,7 +433,7 @@ contract GasTest is Test {
borrower.setAction(FlashBorrower.Action.NORMAL, alice);
// Create loan request for single token (get array size from pool)
address[] memory poolTokens = pool2.allTokens();
IERC20[] memory poolTokens = pool2.allTokens();
uint256[] memory amounts = new uint256[](poolTokens.length);
amounts[0] = 1000;
@@ -440,7 +451,7 @@ contract GasTest is Test {
borrower.setAction(FlashBorrower.Action.NORMAL, alice);
// Create loan request for multiple tokens (get array size from pool)
address[] memory poolTokens = pool2.allTokens();
IERC20[] memory poolTokens = pool2.allTokens();
uint256[] memory amounts = new uint256[](poolTokens.length);
amounts[0] = 1000;
amounts[1] = 2000;

316
test/PartyPlanner.t.sol Normal file
View File

@@ -0,0 +1,316 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import "forge-std/Test.sol";
import "../src/PartyPlanner.sol";
import "../src/PartyPool.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
// Mock ERC20 token for testing
contract MockERC20 is ERC20 {
uint8 private _decimals;
constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) {
_decimals = decimals_;
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
function decimals() public view override returns (uint8) {
return _decimals;
}
}
contract PartyPlannerTest is Test {
PartyPlanner public planner;
MockERC20 public tokenA;
MockERC20 public tokenB;
MockERC20 public tokenC;
address public payer = makeAddr("payer");
address public receiver = makeAddr("receiver");
uint256 constant INITIAL_MINT_AMOUNT = 1000000e18;
uint256 constant INITIAL_DEPOSIT_AMOUNT = 1000e18;
function setUp() public {
// Deploy PartyPlanner
planner = new PartyPlanner();
// Deploy mock tokens
tokenA = new MockERC20("Token A", "TKNA", 18);
tokenB = new MockERC20("Token B", "TKNB", 18);
tokenC = new MockERC20("Token C", "TKNC", 6);
// Mint tokens to payer
tokenA.mint(payer, INITIAL_MINT_AMOUNT);
tokenB.mint(payer, INITIAL_MINT_AMOUNT);
tokenC.mint(payer, INITIAL_MINT_AMOUNT);
// Approve tokens for PartyPlanner
vm.startPrank(payer);
tokenA.approve(address(planner), type(uint256).max);
tokenB.approve(address(planner), type(uint256).max);
tokenC.approve(address(planner), type(uint256).max);
vm.stopPrank();
}
function test_createPool_Success() public {
// Prepare pool parameters
string memory name = "Test Pool";
string memory symbol = "TESTLP";
IERC20[] memory tokens = new IERC20[](2);
tokens[0] = IERC20(address(tokenA));
tokens[1] = IERC20(address(tokenB));
uint256[] memory bases = new uint256[](2);
bases[0] = 1e18; // 18 decimals
bases[1] = 1e18; // 18 decimals
uint256[] memory initialDeposits = new uint256[](2);
initialDeposits[0] = INITIAL_DEPOSIT_AMOUNT;
initialDeposits[1] = INITIAL_DEPOSIT_AMOUNT;
// Fixed point parameters (using simple values for testing)
int128 tradeFrac = int128((1 << 64) - 1); // slightly less than 1.0 in 64.64 fixed point
int128 targetSlippage = int128(1 << 62); // 0.25 in 64.64 fixed point
uint256 swapFeePpm = 3000; // 0.3%
uint256 flashFeePpm = 5000; // 0.5%
uint256 initialPoolCount = planner.poolCount();
uint256 initialTokenACount = planner.poolsByTokenCount(IERC20(address(tokenA)));
uint256 initialTokenBCount = planner.poolsByTokenCount(IERC20(address(tokenB)));
// Create pool
(PartyPool pool, uint256 lpAmount) = planner.createPool(
name,
symbol,
tokens,
bases,
tradeFrac,
targetSlippage,
swapFeePpm,
flashFeePpm,
false, // not stable
payer,
receiver,
initialDeposits,
1000e18, // initial LP amount
0 // no deadline
);
// Verify pool was created
assertNotEq(address(pool), address(0), "Pool should be created");
assertGt(lpAmount, 0, "LP tokens should be minted");
// Verify pool is indexed correctly
assertEq(planner.poolCount(), initialPoolCount + 1, "Pool count should increase by 1");
assertTrue(planner.getPoolSupported(address(pool)), "Pool should be marked as supported");
// Verify token indexing
assertEq(planner.poolsByTokenCount(IERC20(address(tokenA))), initialTokenACount + 1, "TokenA pool count should increase");
assertEq(planner.poolsByTokenCount(IERC20(address(tokenB))), initialTokenBCount + 1, "TokenB pool count should increase");
// Verify pools can be retrieved
PartyPool[] memory allPools = planner.getAllPools(0, 10);
bool poolFound = false;
for (uint256 i = 0; i < allPools.length; i++) {
if (allPools[i] == pool) {
poolFound = true;
break;
}
}
assertTrue(poolFound, "Created pool should be in getAllPools result");
// Verify pool appears in token-specific queries
PartyPool[] memory tokenAPools = planner.getPoolsByToken(IERC20(address(tokenA)), 0, 10);
bool poolInTokenA = false;
for (uint256 i = 0; i < tokenAPools.length; i++) {
if (tokenAPools[i] == pool) {
poolInTokenA = true;
break;
}
}
assertTrue(poolInTokenA, "Pool should be indexed under tokenA");
PartyPool[] memory tokenBPools = planner.getPoolsByToken(IERC20(address(tokenB)), 0, 10);
bool poolInTokenB = false;
for (uint256 i = 0; i < tokenBPools.length; i++) {
if (tokenBPools[i] == pool) {
poolInTokenB = true;
break;
}
}
assertTrue(poolInTokenB, "Pool should be indexed under tokenB");
// Verify LP tokens were minted to receiver
assertEq(pool.balanceOf(receiver), lpAmount, "Receiver should have LP tokens");
}
function test_createPool_MultiplePoolsIndexing() public {
// Create first pool with tokenA and tokenB
IERC20[] memory tokens1 = new IERC20[](2);
tokens1[0] = IERC20(address(tokenA));
tokens1[1] = IERC20(address(tokenB));
uint256[] memory bases1 = new uint256[](2);
bases1[0] = 1e18;
bases1[1] = 1e18;
uint256[] memory deposits1 = new uint256[](2);
deposits1[0] = INITIAL_DEPOSIT_AMOUNT;
deposits1[1] = INITIAL_DEPOSIT_AMOUNT;
(PartyPool pool1,) = planner.createPool(
"Pool 1", "LP1", tokens1, bases1,
int128((1 << 64) - 1), int128(1 << 62), 3000, 5000, false,
payer, receiver, deposits1, 1000e18, 0
);
// Create second pool with tokenB and tokenC
IERC20[] memory tokens2 = new IERC20[](2);
tokens2[0] = IERC20(address(tokenB));
tokens2[1] = IERC20(address(tokenC));
uint256[] memory bases2 = new uint256[](2);
bases2[0] = 1e18;
bases2[1] = 1e6; // tokenC has 6 decimals
uint256[] memory deposits2 = new uint256[](2);
deposits2[0] = INITIAL_DEPOSIT_AMOUNT;
deposits2[1] = INITIAL_DEPOSIT_AMOUNT / 1e12; // Adjust for 6 decimals
(PartyPool pool2,) = planner.createPool(
"Pool 2", "LP2", tokens2, bases2,
int128((1 << 64) - 1), int128(1 << 62), 3000, 5000, false,
payer, receiver, deposits2, 1000e18, 0
);
// Verify indexing
assertEq(planner.poolCount(), 2, "Should have 2 pools");
assertEq(planner.tokenCount(), 3, "Should have 3 unique tokens");
// Verify token-pool relationships
assertEq(planner.poolsByTokenCount(IERC20(address(tokenA))), 1, "TokenA should be in 1 pool");
assertEq(planner.poolsByTokenCount(IERC20(address(tokenB))), 2, "TokenB should be in 2 pools");
assertEq(planner.poolsByTokenCount(IERC20(address(tokenC))), 1, "TokenC should be in 1 pool");
// Verify tokenB appears in both pools
PartyPool[] memory tokenBPools = planner.getPoolsByToken(IERC20(address(tokenB)), 0, 10);
assertEq(tokenBPools.length, 2, "TokenB should have 2 pools");
bool pool1Found = false;
bool pool2Found = false;
for (uint256 i = 0; i < tokenBPools.length; i++) {
if (tokenBPools[i] == pool1) pool1Found = true;
if (tokenBPools[i] == pool2) pool2Found = true;
}
assertTrue(pool1Found, "Pool1 should be in tokenB pools");
assertTrue(pool2Found, "Pool2 should be in tokenB pools");
}
function test_createPool_InvalidInputs() public {
IERC20[] memory tokens = new IERC20[](2);
tokens[0] = IERC20(address(tokenA));
tokens[1] = IERC20(address(tokenB));
uint256[] memory bases = new uint256[](2);
bases[0] = 1e18;
bases[1] = 1e18;
uint256[] memory deposits = new uint256[](1); // Mismatched length
deposits[0] = INITIAL_DEPOSIT_AMOUNT;
// Test token/deposit length mismatch
vm.expectRevert("Planner: tokens and deposits length mismatch");
planner.createPool(
"Test Pool", "TESTLP", tokens, bases,
int128((1 << 64) - 1), int128(1 << 62), 3000, 5000, false,
payer, receiver, deposits, 1000e18, 0
);
// Test zero payer address
uint256[] memory validDeposits = new uint256[](2);
validDeposits[0] = INITIAL_DEPOSIT_AMOUNT;
validDeposits[1] = INITIAL_DEPOSIT_AMOUNT;
vm.expectRevert("Planner: payer cannot be zero address");
planner.createPool(
"Test Pool", "TESTLP", tokens, bases,
int128((1 << 64) - 1), int128(1 << 62), 3000, 5000, false,
address(0), receiver, validDeposits, 1000e18, 0
);
// Test zero receiver address
vm.expectRevert("Planner: receiver cannot be zero address");
planner.createPool(
"Test Pool", "TESTLP", tokens, bases,
int128((1 << 64) - 1), int128(1 << 62), 3000, 5000, false,
payer, address(0), validDeposits, 1000e18, 0
);
// Test deadline exceeded
// The default timestamp is 1 and 1-0 is 0 which means "ignore deadline," so we need to set a proper timestamp.
vm.warp(1000);
vm.expectRevert("Planner: deadline exceeded");
planner.createPool(
"Test Pool", "TESTLP", tokens, bases,
int128((1 << 64) - 1), int128(1 << 62), 3000, 5000, false,
payer, receiver, validDeposits, 1000e18, block.timestamp - 1
);
}
function test_poolIndexing_Pagination() public {
// Create multiple pools for pagination testing
uint256 numPools = 5;
PartyPool[] memory createdPools = new PartyPool[](numPools);
for (uint256 i = 0; i < numPools; i++) {
IERC20[] memory tokens = new IERC20[](2);
tokens[0] = IERC20(address(tokenA));
tokens[1] = IERC20(address(tokenB));
uint256[] memory bases = new uint256[](2);
bases[0] = 1e18;
bases[1] = 1e18;
uint256[] memory deposits = new uint256[](2);
deposits[0] = INITIAL_DEPOSIT_AMOUNT;
deposits[1] = INITIAL_DEPOSIT_AMOUNT;
(PartyPool pool,) = planner.createPool(
string(abi.encodePacked("Pool ", vm.toString(i))),
string(abi.encodePacked("LP", vm.toString(i))),
tokens, bases,
int128((1 << 64) - 1), int128(1 << 62), 3000, 5000, false,
payer, receiver, deposits, 1000e18, 0
);
createdPools[i] = pool;
}
assertEq(planner.poolCount(), numPools, "Should have created all pools");
// Test pagination - get first 3 pools
PartyPool[] memory page1 = planner.getAllPools(0, 3);
assertEq(page1.length, 3, "First page should have 3 pools");
// Test pagination - get next 2 pools
PartyPool[] memory page2 = planner.getAllPools(3, 3);
assertEq(page2.length, 2, "Second page should have 2 pools");
// Test pagination - offset beyond bounds
PartyPool[] memory emptyPage = planner.getAllPools(10, 3);
assertEq(emptyPage.length, 0, "Should return empty array for out of bounds offset");
// Verify all pools are accessible through pagination
PartyPool[] memory allPools = planner.getAllPools(0, 10);
assertEq(allPools.length, numPools, "Should return all pools");
for (uint256 i = 0; i < numPools; i++) {
assertEq(address(allPools[i]), address(createdPools[i]), "Pool order should be preserved");
}
}
}

View File

@@ -182,10 +182,10 @@ contract PartyPoolTest is Test {
targetSlippage = ABDKMath64x64.divu(10, 10_000); // 0.001
// Build arrays for pool constructor
address[] memory tokens = new address[](3);
tokens[0] = address(token0);
tokens[1] = address(token1);
tokens[2] = address(token2);
IERC20[] memory tokens = new IERC20[](3);
tokens[0] = IERC20(address(token0));
tokens[1] = IERC20(address(token1));
tokens[2] = IERC20(address(token2));
uint256[] memory bases = new uint256[](3);
bases[0] = BASE;
@@ -204,20 +204,20 @@ contract PartyPoolTest is Test {
token2.transfer(address(pool), INIT_BAL);
// Perform initial mint (initial deposit); receiver is this contract
pool.mint(address(0), address(this), 0, 0);
pool.initialMint(address(this), 0);
// Set up pool10 with 10 tokens
address[] memory tokens10 = new address[](10);
tokens10[0] = address(token0);
tokens10[1] = address(token1);
tokens10[2] = address(token2);
tokens10[3] = address(token3);
tokens10[4] = address(token4);
tokens10[5] = address(token5);
tokens10[6] = address(token6);
tokens10[7] = address(token7);
tokens10[8] = address(token8);
tokens10[9] = address(token9);
IERC20[] memory tokens10 = new IERC20[](10);
tokens10[0] = IERC20(address(token0));
tokens10[1] = IERC20(address(token1));
tokens10[2] = IERC20(address(token2));
tokens10[3] = IERC20(address(token3));
tokens10[4] = IERC20(address(token4));
tokens10[5] = IERC20(address(token5));
tokens10[6] = IERC20(address(token6));
tokens10[7] = IERC20(address(token7));
tokens10[8] = IERC20(address(token8));
tokens10[9] = IERC20(address(token9));
uint256[] memory bases10 = new uint256[](10);
for (uint i = 0; i < 10; i++) {
@@ -251,7 +251,7 @@ contract PartyPoolTest is Test {
token9.transfer(address(pool10), INIT_BAL);
// Perform initial mint for pool10
pool10.mint(address(0), address(this), 0, 0);
pool10.initialMint(address(this), 0);
// For later tests we will mint tokens to alice/bob as needed
token0.mint(alice, INIT_BAL);
@@ -349,7 +349,7 @@ contract PartyPoolTest is Test {
token2.approve(address(pool), type(uint256).max);
// Snapshot pool totals (simple value metric = sum of token uint balances since base==1 in tests)
address[] memory toks = pool.allTokens();
IERC20[] memory toks = pool.allTokens();
uint256 n = toks.length;
uint256 poolValueBefore = 0;
for (uint i = 0; i < n; i++) {
@@ -1210,4 +1210,171 @@ contract PartyPoolTest is Test {
pool.flash(alice, wrongLengthAmounts, "");
}
/// @notice Test that passing nonzero lpTokens to initialMint doesn't affect swap results
/// compared to pools initialized with default lpTokens (0)
function testInitialMintCustomLpTokensDoesNotAffectSwaps() public {
// Create two identical pools with different initial LP amounts
IERC20[] memory tokens = new IERC20[](3);
tokens[0] = IERC20(address(token0));
tokens[1] = IERC20(address(token1));
tokens[2] = IERC20(address(token2));
uint256[] memory bases = new uint256[](3);
bases[0] = BASE;
bases[1] = BASE;
bases[2] = BASE;
uint256 feePpm = 1000;
// Pool with default initialization (lpTokens = 0)
PartyPool poolDefault = new PartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, tradeFrac, targetSlippage, feePpm, feePpm, false);
// Pool with custom initialization (lpTokens = custom amount)
PartyPool poolCustom = new PartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, tradeFrac, targetSlippage, feePpm, feePpm, false);
// Mint additional tokens for both pools
token0.mint(address(this), INIT_BAL * 2);
token1.mint(address(this), INIT_BAL * 2);
token2.mint(address(this), INIT_BAL * 2);
// Transfer identical amounts to both pools
token0.transfer(address(poolDefault), INIT_BAL);
token1.transfer(address(poolDefault), INIT_BAL);
token2.transfer(address(poolDefault), INIT_BAL);
token0.transfer(address(poolCustom), INIT_BAL);
token1.transfer(address(poolCustom), INIT_BAL);
token2.transfer(address(poolCustom), INIT_BAL);
// Initialize poolDefault with lpTokens = 0 (default behavior)
uint256 lpDefault = poolDefault.initialMint(address(this), 0);
// Initialize poolCustom with custom lpTokens amount (5x the default)
uint256 customLpAmount = lpDefault * 5;
uint256 lpCustom = poolCustom.initialMint(address(this), customLpAmount);
// Verify the custom pool has the expected LP supply
assertEq(lpCustom, customLpAmount, "Custom pool should have expected LP amount");
assertEq(poolCustom.totalSupply(), customLpAmount, "Custom pool total supply should match");
// Both pools should have identical token balances
assertEq(token0.balanceOf(address(poolDefault)), token0.balanceOf(address(poolCustom)), "Token0 balances should match");
assertEq(token1.balanceOf(address(poolDefault)), token1.balanceOf(address(poolCustom)), "Token1 balances should match");
assertEq(token2.balanceOf(address(poolDefault)), token2.balanceOf(address(poolCustom)), "Token2 balances should match");
// Prepare Alice for swapping
token0.mint(alice, INIT_BAL);
token1.mint(alice, INIT_BAL);
// Test identical swaps produce identical results
uint256 swapAmount = 10_000;
vm.startPrank(alice);
token0.approve(address(poolDefault), type(uint256).max);
token0.approve(address(poolCustom), type(uint256).max);
// Perform identical swaps: token0 -> token1
(uint256 amountInDefault, uint256 amountOutDefault, uint256 feeDefault) = poolDefault.swap(alice, alice, 0, 1, swapAmount, 0, 0);
(uint256 amountInCustom, uint256 amountOutCustom, uint256 feeCustom) = poolCustom.swap(alice, alice, 0, 1, swapAmount, 0, 0);
// Swap results should be identical
assertEq(amountInDefault, amountInCustom, "Swap input amounts should be identical");
assertEq(amountOutDefault, amountOutCustom, "Swap output amounts should be identical");
assertEq(feeDefault, feeCustom, "Swap fees should be identical");
vm.stopPrank();
}
/// @notice Test that minting the same proportion in pools with different initial LP amounts
/// returns correctly scaled LP tokens
function testProportionalMintingScaledByInitialAmount() public {
// Create two identical pools with different initial LP amounts
IERC20[] memory tokens = new IERC20[](3);
tokens[0] = IERC20(address(token0));
tokens[1] = IERC20(address(token1));
tokens[2] = IERC20(address(token2));
uint256[] memory bases = new uint256[](3);
bases[0] = BASE;
bases[1] = BASE;
bases[2] = BASE;
uint256 feePpm = 1000;
PartyPool poolDefault = new PartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, tradeFrac, targetSlippage, feePpm, feePpm, false);
PartyPool poolCustom = new PartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, tradeFrac, targetSlippage, feePpm, feePpm, false);
// Mint additional tokens
token0.mint(address(this), INIT_BAL * 4);
token1.mint(address(this), INIT_BAL * 4);
token2.mint(address(this), INIT_BAL * 4);
// Transfer identical amounts to both pools
token0.transfer(address(poolDefault), INIT_BAL);
token1.transfer(address(poolDefault), INIT_BAL);
token2.transfer(address(poolDefault), INIT_BAL);
token0.transfer(address(poolCustom), INIT_BAL);
token1.transfer(address(poolCustom), INIT_BAL);
token2.transfer(address(poolCustom), INIT_BAL);
// Initialize pools with different LP amounts
uint256 lpDefault = poolDefault.initialMint(address(this), 0);
uint256 scaleFactor = 3;
uint256 customLpAmount = lpDefault * scaleFactor;
uint256 lpCustom = poolCustom.initialMint(address(this), customLpAmount);
// Verify initial LP supplies
assertEq(poolDefault.totalSupply(), lpDefault, "Default pool should have default LP supply");
assertEq(poolCustom.totalSupply(), customLpAmount, "Custom pool should have custom LP supply");
// Prepare Alice for minting
token0.mint(alice, INIT_BAL * 2);
token1.mint(alice, INIT_BAL * 2);
token2.mint(alice, INIT_BAL * 2);
// Test proportional minting: mint 10% of each pool's supply
uint256 mintPercentage = 10; // 10%
uint256 lpRequestDefault = poolDefault.totalSupply() * mintPercentage / 100;
uint256 lpRequestCustom = poolCustom.totalSupply() * mintPercentage / 100;
vm.startPrank(alice);
// Approve tokens for both pools
token0.approve(address(poolDefault), type(uint256).max);
token1.approve(address(poolDefault), type(uint256).max);
token2.approve(address(poolDefault), type(uint256).max);
token0.approve(address(poolCustom), type(uint256).max);
token1.approve(address(poolCustom), type(uint256).max);
token2.approve(address(poolCustom), type(uint256).max);
// Get required deposit amounts for both pools
uint256[] memory depositsDefault = poolDefault.mintDepositAmounts(lpRequestDefault);
uint256[] memory depositsCustom = poolCustom.mintDepositAmounts(lpRequestCustom);
// Deposits should be identical (same proportion of identical balances)
assertEq(depositsDefault[0], depositsCustom[0], "Token0 deposits should be identical");
assertEq(depositsDefault[1], depositsCustom[1], "Token1 deposits should be identical");
assertEq(depositsDefault[2], depositsCustom[2], "Token2 deposits should be identical");
// Perform the mints
uint256 mintedDefault = poolDefault.mint(alice, alice, lpRequestDefault, 0);
uint256 mintedCustom = poolCustom.mint(alice, alice, lpRequestCustom, 0);
// Minted LP amounts should be scaled by the same factor as initial supplies
uint256 expectedRatio = (mintedCustom * 1000) / mintedDefault; // Use fixed point for precision
uint256 actualRatio = (scaleFactor * 1000);
// Allow small rounding differences (within 0.1%)
uint256 tolerance = actualRatio / 1000; // 0.1% tolerance
assertTrue(expectedRatio >= actualRatio - tolerance && expectedRatio <= actualRatio + tolerance,
"Minted LP ratio should match scale factor within tolerance");
// Verify Alice received the expected LP amounts
assertTrue(poolDefault.balanceOf(alice) >= mintedDefault, "Alice should receive default LP");
assertTrue(poolCustom.balanceOf(alice) >= mintedCustom, "Alice should receive custom LP");
vm.stopPrank();
}
}