From 9273430f2a363b6fd97875d6b84f1f75a08b3d9b Mon Sep 17 00:00:00 2001 From: tim Date: Thu, 13 Nov 2025 16:41:52 -0400 Subject: [PATCH] CREATE2 callback validation; init code storage contracts --- bin/gas-report | 15 +- foundry.toml | 2 +- script/DeployMock.sol | 25 ++- script/DeploySepolia.sol | 20 +-- script/InitCodeHashes.sol | 16 ++ src/ERC20External.sol | 2 +- src/ERC20Internal.sol | 2 +- src/Funding.sol | 2 +- src/IPartyPlanner.sol | 40 ++++- src/IPartyPool.sol | 12 +- src/IPartyPoolDeployer.sol | 45 +++++ src/IPartySwapCallback.sol | 10 ++ src/OwnableExternal.sol | 2 +- src/PartyInfo.sol | 4 +- src/PartyPlanner.sol | 33 ++-- src/PartyPool.sol | 114 +++++------- src/PartyPoolBalancedPair.sol | 17 -- src/PartyPoolBase.sol | 52 ++++-- src/PartyPoolDeployer.sol | 162 ++++++++--------- src/PartyPoolMintImpl.sol | 2 +- src/PartyPoolSwapImpl.sol | 17 +- src/PartySwapCallbackVerifier.sol | 27 +++ test/Deploy.sol | 146 ++++++++++------ test/FundingSwapTest.sol | 278 ++++++++++++++++-------------- test/GasTest.sol | 151 +++++++++++----- test/NativeTest.t.sol | 52 +++--- test/PartyPlanner.t.sol | 7 +- test/PartyPool.t.sol | 112 +++--------- 28 files changed, 779 insertions(+), 588 deletions(-) create mode 100644 script/InitCodeHashes.sol create mode 100644 src/IPartyPoolDeployer.sol create mode 100644 src/IPartySwapCallback.sol create mode 100644 src/PartySwapCallbackVerifier.sol diff --git a/bin/gas-report b/bin/gas-report index 74f32d0..fe83dc1 100755 --- a/bin/gas-report +++ b/bin/gas-report @@ -3,7 +3,13 @@ report() { local name=${2:-$1} REPORT=$(forge test --mc GasTest --mt "$1" --gas-report) - SWAP=$(echo "$REPORT" | grep 'swap ' | cut -d '|' -f 5 | xargs) + SWAP=$(echo "$REPORT" | grep 'swapApproval ' | cut -d '|' -f 5 | xargs) + if [ -z "$SWAP" ]; then + SWAP=$(echo "$REPORT" | grep 'swapPrefund ' | cut -d '|' -f 5 | xargs) + fi + if [ -z "$SWAP" ]; then + SWAP=$(echo "$REPORT" | grep 'swapCallback ' | cut -d '|' -f 5 | xargs) + fi MINT=$(echo "$REPORT" | grep 'mint ' | cut -d '|' -f 5 | xargs) SWAPMINT=$(echo "$REPORT" | grep 'swapMint ' | cut -d '|' -f 5 | xargs) printf "%-15s %10s %10s %10s\n" "$name" "$SWAP" "$MINT" "$SWAPMINT" @@ -15,8 +21,11 @@ printf "%s\n" " ------ --------- ---------" report GasPair Pair report StablePair "Stable Pair" +report PrefundingSP "SPair prefund" report Ten -report Callback "Ten-callback" -report Prefunding "Ten-prefunding" +report Prefunding10 "Ten prefund" +report Callback10 "Ten callback" report Twenty +report Prefunding20 "Twenty prefund" +report Callback20 "Twenty callback" report Fifty diff --git a/foundry.toml b/foundry.toml index b49768a..5aaeca4 100644 --- a/foundry.toml +++ b/foundry.toml @@ -10,7 +10,7 @@ optimizer=true optimizer_runs=100000000 # maximum value allowed by etherscan's verifier XD. The max value is formally 2^32-1 viaIR=true #gas_reports = ['PartyPool', 'PartyPoolBalancedPair', 'PartyPlanner', 'PartyPoolSwapImpl', 'PartyPoolMintImpl',] -gas_reports = ['PartyPool', 'PartyPoolBalancedPair', 'PartyPlanner'] +gas_reports = ['PartyPool', 'PartyPoolBalancedPair', 'PartyPlanner', 'GasHarness'] fs_permissions = [{ access = "write", path = "liqp-deployments.json"}] [lint] diff --git a/script/DeployMock.sol b/script/DeployMock.sol index 60791f7..8b1ca4c 100644 --- a/script/DeployMock.sol +++ b/script/DeployMock.sol @@ -10,10 +10,17 @@ import {StdCheatsSafe} from "../lib/forge-std/src/StdCheats.sol"; import {StdUtils} from "../lib/forge-std/src/StdUtils.sol"; import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {IERC20Metadata} from "../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import {PartyPlanner} from "../src/PartyPlanner.sol"; +import {IPartyInfo} from "../src/IPartyInfo.sol"; +import {IPartyPlanner} from "../src/IPartyPlanner.sol"; +import {NativeWrapper} from "../src/NativeWrapper.sol"; import {PartyInfo} from "../src/PartyInfo.sol"; +import {PartyPlanner} from "../src/PartyPlanner.sol"; +import {PartyPoolInitCode, PartyPoolBalancedPairInitCode} from "../src/PartyPoolDeployer.sol"; +import {PartyPoolMintImpl} from "../src/PartyPoolMintImpl.sol"; +import {PartyPoolSwapImpl} from "../src/PartyPoolSwapImpl.sol"; import {Deploy} from "../test/Deploy.sol"; import {MockERC20} from "../test/MockERC20.sol"; +import {WETH9} from "../test/WETH9.sol"; contract DeployMock is Script { @@ -34,7 +41,19 @@ contract DeployMock is Script { wteth = new MockERC20('Wrapped TETH', 'WTETH', 18); // deploy a PartyPlanner factory and create the pool via factory - PartyPlanner planner = Deploy.newPartyPlanner(); + NativeWrapper wrapper = new WETH9(); + PartyPoolSwapImpl swapImpl = new PartyPoolSwapImpl(wrapper); + PartyPoolMintImpl mintImpl = new PartyPoolMintImpl(wrapper); + IPartyPlanner planner = new PartyPlanner( + msg.sender, + wrapper, + swapImpl, + new PartyPoolMintImpl(wrapper), + new PartyPoolInitCode(), + new PartyPoolBalancedPairInitCode(), + Deploy.PROTOCOL_FEE_PPM, + Deploy.PROTOCOL_FEE_RECEIVER + ); // // Deploy 3-asset pool @@ -163,7 +182,7 @@ contract DeployMock is Script { 0 ); - PartyInfo info = Deploy.newInfo(); + IPartyInfo info = new PartyInfo(swapImpl, mintImpl); // give _tokens to dev7 for later use mintAll(DEV_ACCOUNT_7, 1_000_000); diff --git a/script/DeploySepolia.sol b/script/DeploySepolia.sol index fe84de0..3a6c613 100644 --- a/script/DeploySepolia.sol +++ b/script/DeploySepolia.sol @@ -10,15 +10,15 @@ import {StdCheatsSafe} from "../lib/forge-std/src/StdCheats.sol"; import {StdUtils} from "../lib/forge-std/src/StdUtils.sol"; import {IERC3156FlashBorrower} from "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sol"; import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import {IPartyPool} from "../src/IPartyPool.sol"; import {IPartyInfo} from "../src/IPartyInfo.sol"; +import {IPartyPool} from "../src/IPartyPool.sol"; import {LMSRStabilized} from "../src/LMSRStabilized.sol"; import {NativeWrapper} from "../src/NativeWrapper.sol"; +import {PartyInfo} from "../src/PartyInfo.sol"; import {PartyPlanner} from "../src/PartyPlanner.sol"; -import {PartyPoolDeployer, PartyPoolBalancedPairDeployer} from "../src/PartyPoolDeployer.sol"; +import {PartyPoolInitCode, PartyPoolBalancedPairInitCode} from "../src/PartyPoolDeployer.sol"; import {PartyPoolMintImpl} from "../src/PartyPoolMintImpl.sol"; import {PartyPoolSwapImpl} from "../src/PartyPoolSwapImpl.sol"; -import {PartyInfo} from "../src/PartyInfo.sol"; import {MockERC20} from "../test/MockERC20.sol"; import {MockFlashBorrower} from "../test/MockFlashBorrower.sol"; @@ -55,8 +55,8 @@ contract DeploySepolia is Script { PartyPoolSwapImpl swapImpl = new PartyPoolSwapImpl(WETH); PartyPoolMintImpl mintImpl = new PartyPoolMintImpl(WETH); - PartyPoolDeployer deployer = new PartyPoolDeployer(); - PartyPoolBalancedPairDeployer balancedPairDeployer = new PartyPoolBalancedPairDeployer(); + PartyPoolInitCode poolInit = new PartyPoolInitCode(); + PartyPoolBalancedPairInitCode bpInit = new PartyPoolBalancedPairInitCode(); // deploy a PartyPlanner factory and create the pool via factory PartyPlanner planner = new PartyPlanner( @@ -64,8 +64,8 @@ contract DeploySepolia is Script { WETH, swapImpl, mintImpl, - deployer, - balancedPairDeployer, + poolInit, + bpInit, PROTOCOL_FEE_PPM, PROTOCOL_FEE_ADDRESS ); @@ -211,8 +211,8 @@ contract DeploySepolia is Script { console2.log(' PartyInfo', address(info)); console2.log(' SwapImpl', address(swapImpl)); console2.log(' MintImpl', address(mintImpl)); - console2.log(' Deployer', address(deployer)); - console2.log('BPair Deployer', address(balancedPairDeployer)); + console2.log(' PoolCode', address(poolInit)); + console2.log(' BPPoolCode', address(bpInit)); console2.log(); console2.log(' USXD', address(usxd)); console2.log(' FUSD', address(fusd)); @@ -284,7 +284,7 @@ contract DeploySepolia is Script { uint256 inputIndex = 0; uint256 outputIndex = n > 1 ? n - 1 : 0; uint256 maxIn = 89 * 10**6; // varied - pool.swap(msg.sender, bytes4(0), msg.sender, inputIndex, outputIndex, maxIn, int128(0), 0, false); + pool.swap(msg.sender, bytes4(0), msg.sender, inputIndex, outputIndex, maxIn, int128(0), 0, false, ''); // 6) Collect protocol fees now (after some swaps) so some will have been moved out pool.collectProtocolFees(); diff --git a/script/InitCodeHashes.sol b/script/InitCodeHashes.sol new file mode 100644 index 0000000..8df1b1d --- /dev/null +++ b/script/InitCodeHashes.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.30; + +import "forge-std/console2.sol"; +import {Script} from "../lib/forge-std/src/Script.sol"; +import {PartyPool} from "../src/PartyPool.sol"; +import {PartyPoolBalancedPair} from "../src/PartyPoolBalancedPair.sol"; + +contract InitCodeHashes is Script { + function run() public pure { + console2.log('\nPool Init Code Hash'); + console2.logBytes32(keccak256(type(PartyPool).creationCode)); + console2.log('\nBP Pool Init Code Hash'); + console2.logBytes32(keccak256(type(PartyPoolBalancedPair).creationCode)); + } +} diff --git a/src/ERC20External.sol b/src/ERC20External.sol index d716dcc..039856b 100644 --- a/src/ERC20External.sol +++ b/src/ERC20External.sol @@ -15,7 +15,7 @@ contract ERC20External is ERC20Internal, IERC20Metadata { * * Both values are immutable: they can only be set once during construction. */ - constructor(string memory name_, string memory symbol_) { + function erc20Constructor(string memory name_, string memory symbol_) internal { _name = name_; _symbol = symbol_; } diff --git a/src/ERC20Internal.sol b/src/ERC20Internal.sol index 1bb37d6..30f84ac 100644 --- a/src/ERC20Internal.sol +++ b/src/ERC20Internal.sol @@ -15,7 +15,7 @@ abstract contract ERC20Internal is Context, IERC20Errors { string internal _symbol; - /** + /** * @dev Moves a `value` amount of _tokens from `from` to `to`. * * This internal function is equivalent to {transfer}, and can be used to diff --git a/src/Funding.sol b/src/Funding.sol index a693010..6536f9a 100644 --- a/src/Funding.sol +++ b/src/Funding.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.30; library Funding { /// @notice a constant passed to swap as the fundingSelector to indicate that the payer has used regular ERC20 approvals to allow the pool to move the necessary input tokens. - bytes4 internal constant APPROVALS = 0x00000000; + bytes4 internal constant APPROVAL = 0x00000000; /// @notice a constant passed to swap as the fundingSelector to indicate that the payer has already sent sufficient input tokens to the pool before calling swap, so no movement of input tokens is required. bytes4 internal constant PREFUNDING = 0x00000001; diff --git a/src/IPartyPlanner.sol b/src/IPartyPlanner.sol index 57c55eb..5c67b4a 100644 --- a/src/IPartyPlanner.sol +++ b/src/IPartyPlanner.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.30; import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -import "./IPartyPool.sol"; -import "./PartyPoolMintImpl.sol"; -import "./PartyPoolSwapImpl.sol"; +import {IOwnable} from "./IOwnable.sol"; +import {IPartyPool} from "./IPartyPool.sol"; +import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol"; +import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol"; /// @title IPartyPlanner /// @notice Interface for factory contract for creating and tracking PartyPool instances @@ -13,6 +13,38 @@ interface IPartyPlanner is IOwnable { // Event emitted when a new pool is created event PartyStarted(IPartyPool indexed pool, string name, string symbol, IERC20[] tokens); + + /// @notice Primary method for creating a new pool. May only be called by the PartyPlanner owner account. + /// @param name LP token name + /// @param symbol LP token symbol + /// @param tokens token addresses + /// @param kappa liquidity parameter κ in 64.64 fixed-point used to derive b = κ * S(q) + /// @param swapFeesPpm per-asset fees in parts-per-million, taken from swap input amounts before LMSR calculations + /// @param flashFeePpm fee in parts-per-million, taken for flash loans + /// @param stable if true and assets.length==2, then the optimization for 2-asset stablecoin pools is activated + /// @param payer address that provides the initial token deposits + /// @param receiver address that receives the minted LP tokens + /// @param initialDeposits amounts of each token to deposit initially + /// @param deadline Reverts if nonzero and the current blocktime is later than the deadline + /// @return pool Address of the newly created and initialized PartyPool + /// @return lpAmount Amount of LP tokens minted to the receiver + function newPool( + // Pool constructor args + string memory name, + string memory symbol, + IERC20[] memory tokens, + int128 kappa, + uint256[] memory swapFeesPpm, + uint256 flashFeePpm, + bool stable, + // Initial deposit information + address payer, + address receiver, + uint256[] memory initialDeposits, + uint256 initialLpAmount, + uint256 deadline + ) external returns (IPartyPool pool, uint256 lpAmount); + /// @notice Creates a new PartyPool instance and initializes it with initial deposits (legacy signature). /// @dev Deprecated in favour of the kappa-based overload below; kept for backwards compatibility. /// @param name LP token name diff --git a/src/IPartyPool.sol b/src/IPartyPool.sol index a84ac20..5060fda 100644 --- a/src/IPartyPool.sol +++ b/src/IPartyPool.sol @@ -82,7 +82,7 @@ interface IPartyPool is IERC20Metadata, IOwnable { /// @notice Token addresses comprising the pool. Effectively immutable after construction. /// @dev tokens[i] corresponds to the i-th asset and maps to index i in the internal LMSR arrays. - function getToken(uint256) external view returns (IERC20); // get single token + function token(uint256) external view returns (IERC20); // get single token /// @notice Returns the number of tokens (n) in the pool. function numTokens() external view returns (uint256); @@ -185,6 +185,8 @@ interface IPartyPool is IERC20Metadata, IOwnable { /// @param maxAmountIn maximum amount of token inputTokenIndex (uint256) to transfer in (inclusive of fees) /// @param limitPrice maximum acceptable marginal price (64.64 fixed point). Pass 0 to ignore. /// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore. + /// @param unwrap If true, then any output of wrapper token will be unwrapped and native ETH sent to the receiver. + /// @param cbData callback data if fundingSelector is of the callback type. /// @return amountIn actual input used (uint256), amountOut actual output sent (uint256), inFee fee taken from the input (uint256) function swap( address payer, @@ -195,9 +197,11 @@ interface IPartyPool is IERC20Metadata, IOwnable { uint256 maxAmountIn, int128 limitPrice, uint256 deadline, - bool unwrap + bool unwrap, + bytes memory cbData ) external payable returns (uint256 amountIn, uint256 amountOut, uint256 inFee); + /// @notice Swap up to the price limit; computes max input to reach limit then performs swap. /// @dev If balances prevent fully reaching the limit, the function caps and returns actuals. /// The payer must transfer the exact gross input computed by the view. @@ -210,12 +214,14 @@ interface IPartyPool is IERC20Metadata, IOwnable { /// @return amountInUsed actual input used excluding fee (uint256), amountOut actual output sent (uint256), inFee fee taken from the input (uint256) function swapToLimit( address payer, + bytes4 fundingSelector, address receiver, uint256 inputTokenIndex, uint256 outputTokenIndex, int128 limitPrice, uint256 deadline, - bool unwrap + bool unwrap, + bytes memory cbData ) external payable returns (uint256 amountInUsed, uint256 amountOut, uint256 inFee); /// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP. diff --git a/src/IPartyPoolDeployer.sol b/src/IPartyPoolDeployer.sol new file mode 100644 index 0000000..90ebe0b --- /dev/null +++ b/src/IPartyPoolDeployer.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.30; + +import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {IPartyPool} from "./IPartyPool.sol"; +import {NativeWrapper} from "./NativeWrapper.sol"; +import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol"; +import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol"; + +interface IPartyPoolDeployer { + + /// @notice Parameters for deploying a new PartyPool + struct DeployParams { + /// @notice Used for callback validation + bytes32 nonce; + /// @notice Admin account that can disable the vault using kill() + address owner; + /// @notice LP token name + string name; + /// @notice LP token symbol + string symbol; + /// @notice Token addresses (n) + IERC20[] tokens; + /// @notice Liquidity parameter κ (Q64.64) used to derive b = κ * S(q) + int128 kappa; + /// @notice Per-asset swap fees in ppm (length must equal tokens.length) + uint256[] fees; + /// @notice Fee in parts-per-million, taken for flash loans + uint256 flashFeePpm; + /// @notice Protocol fee in parts-per-million + uint256 protocolFeePpm; + /// @notice Address to receive protocol fees + address protocolFeeAddress; + /// @notice Native token wrapper contract + NativeWrapper wrapper; + /// @notice Address of the SwapMint implementation contract + PartyPoolSwapImpl swapImpl; + /// @notice Address of the Mint implementation contract + PartyPoolMintImpl mintImpl; + } + + function params() external view returns (DeployParams memory); +} + + diff --git a/src/IPartySwapCallback.sol b/src/IPartySwapCallback.sol new file mode 100644 index 0000000..b47bc27 --- /dev/null +++ b/src/IPartySwapCallback.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.30; + +import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; + +interface IPartySwapCallback { + // The callback may have any function name. Pass your callback function selector to the swap method as the fundingSelector + function liquidityPartySwapCallback(bytes32 nonce, IERC20 inputToken, uint256 amount, bytes memory data) external; +} + diff --git a/src/OwnableExternal.sol b/src/OwnableExternal.sol index 089abc6..85f2354 100644 --- a/src/OwnableExternal.sol +++ b/src/OwnableExternal.sol @@ -23,7 +23,7 @@ abstract contract OwnableExternal is OwnableInternal, IOwnable { /** * @dev Initializes the contract setting the address provided by the deployer as the initial owner. */ - constructor(address initialOwner) { + function ownableConstructor(address initialOwner) internal { if (initialOwner == address(0)) { revert OwnableInvalidOwner(address(0)); } diff --git a/src/PartyInfo.sol b/src/PartyInfo.sol index fe6efd1..ec47650 100644 --- a/src/PartyInfo.sol +++ b/src/PartyInfo.sol @@ -76,7 +76,7 @@ contract PartyInfo is PartyPoolHelpers, IPartyInfo { uint256 nAssets = lmsr.qInternal.length; uint256[] memory cachedUintBalances = new uint256[](nAssets); for( uint256 i=0; i= ppm"); PROTOCOL_FEE_PPM = protocolFeePpm_; @@ -111,8 +109,8 @@ contract PartyPlanner is OwnableExternal, IPartyPlanner { require(swapFeesPpm_.length == tokens_.length, "Planner: fees and tokens length mismatch"); // Create a new PartyPool instance (kappa-based constructor) - IPartyPoolDeployer deployer = stable_ && tokens_.length == 2 ? BALANCED_PAIR_DEPLOYER : NORMAL_POOL_DEPLOYER; - pool = deployer.deploy( + IPartyPoolDeployer.DeployParams memory params = IPartyPoolDeployer.DeployParams( + 0, // This is set by the deployer _owner, // Same owner as this PartyPlanner name_, symbol_, @@ -127,6 +125,13 @@ contract PartyPlanner is OwnableExternal, IPartyPlanner { MINT_IMPL ); + // Use inherited deploy methods based on pool type + if (stable_ && tokens_.length == 2) { + pool = _deployBalancedPair(params); + } else { + pool = _deploy(params); + } + _allPools.push(pool); _poolSupported[pool] = true; diff --git a/src/PartyPool.sol b/src/PartyPool.sol index 724fa5d..f1bb46a 100644 --- a/src/PartyPool.sol +++ b/src/PartyPool.sol @@ -17,6 +17,7 @@ import {OwnableInternal} from "./OwnableInternal.sol"; import {PartyPoolBase} from "./PartyPoolBase.sol"; import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol"; import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol"; +import {IPartyPoolDeployer} from "./IPartyPoolDeployer.sol"; /// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token /// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model. @@ -43,7 +44,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool /// @notice If true, the vault has been disabled by the owner and only burns (withdrawals) are allowed. function killed() external view returns (bool) { return _killed; } - function wrapperToken() external view returns (NativeWrapper) { return WRAPPER_TOKEN; } + function wrapperToken() external view returns (NativeWrapper) { return WRAPPER; } /// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q) /// @dev Pool is constructed with a fixed κ. Clients that previously passed tradeFrac/targetSlippage @@ -80,7 +81,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool function swapMintImpl() external view returns (PartyPoolSwapImpl) { return SWAP_IMPL; } /// @inheritdoc IPartyPool - function getToken(uint256 i) external view returns (IERC20) { return _tokens[i]; } + function token(uint256 i) external view returns (IERC20) { return _tokens[i]; } /// @inheritdoc IPartyPool function numTokens() external view returns (uint256) { return _tokens.length; } @@ -94,61 +95,42 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool /// @inheritdoc IPartyPool function LMSR() external view returns (LMSRStabilized.State memory) { return _lmsr; } - - /// @param owner_ Admin account that can disable the vault using kill() - /// @param name_ LP token name - /// @param symbol_ LP token symbol - /// @param tokens_ token addresses (n) - /// @param kappa_ liquidity parameter κ (Q64.64) used to derive b = κ * S(q) - /// @param fees_ per-asset swap fees in ppm (length must equal tokens_.length) - /// @param flashFeePpm_ fee in parts-per-million, taken for flash loans - /// @param swapImpl_ address of the SwapMint implementation contract - /// @param mintImpl_ address of the Mint implementation contract - constructor( - address owner_, - string memory name_, - string memory symbol_, - IERC20[] memory tokens_, - int128 kappa_, - uint256[] memory fees_, - uint256 flashFeePpm_, - uint256 protocolFeePpm_, - address protocolFeeAddress_, - NativeWrapper wrapperToken_, - PartyPoolSwapImpl swapImpl_, - PartyPoolMintImpl mintImpl_ - ) - PartyPoolBase(wrapperToken_) - OwnableExternal(owner_) - ERC20External(name_, symbol_) + constructor() { - require(owner_ != address(0)); - require(tokens_.length > 1, "Pool: need >1 asset"); - _tokens = tokens_; - KAPPA = kappa_; - require(fees_.length == tokens_.length, "Pool: fees length"); - // validate ppm bounds and assign - _fees = new uint256[](fees_.length); - for (uint256 i = 0; i < fees_.length; i++) { - // Cap all fees at 1% - require(fees_[i] < 10_000, "Pool: fee >= 1%"); - _fees[i] = fees_[i]; - } - require(flashFeePpm_ < 10_000, "Pool: flash fee >= 1%"); - FLASH_FEE_PPM = flashFeePpm_; - require(protocolFeePpm_ < 400_000, "Pool: protocol fee >= 40%"); - // If the protocolFeePpm_ is set, then also require the fee address to be nonzero - require(protocolFeePpm_ == 0 || protocolFeeAddress_ != address(0)); - PROTOCOL_FEE_PPM = protocolFeePpm_; - protocolFeeAddress = protocolFeeAddress_; - SWAP_IMPL = swapImpl_; - MINT_IMPL = mintImpl_; + IPartyPoolDeployer.DeployParams memory p = IPartyPoolDeployer(msg.sender).params(); + uint256 n = p.tokens.length; + require(n > 1, "Pool: need >1 asset"); - uint256 n = tokens_.length; + _nonce = p.nonce; + WRAPPER = p.wrapper; + _name = p.name; + _symbol = p.symbol; + + ownableConstructor(p.owner); + + _tokens = p.tokens; + KAPPA = p.kappa; + require(p.fees.length == p.tokens.length, "Pool: fees length"); + // validate ppm bounds and assign + _fees = new uint256[](p.fees.length); + for (uint256 i = 0; i < p.fees.length; i++) { + // Cap all fees at 1% + require(p.fees[i] < 10_000, "Pool: fee >= 1%"); + _fees[i] = p.fees[i]; + } + require(p.flashFeePpm < 10_000, "Pool: flash fee >= 1%"); + FLASH_FEE_PPM = p.flashFeePpm; + require(p.protocolFeePpm < 400_000, "Pool: protocol fee >= 40%"); + // If the p.protocolFeePpm is set, then also require the fee address to be nonzero + require(p.protocolFeePpm == 0 || p.protocolFeeAddress != address(0)); + PROTOCOL_FEE_PPM = p.protocolFeePpm; + protocolFeeAddress = p.protocolFeeAddress; + SWAP_IMPL = p.swapImpl; + MINT_IMPL = p.mintImpl; // Initialize token address to index mapping for (uint i = 0; i < n;) { - _tokenAddressToIndexPlusOne[tokens_[i]] = i + 1; + _tokenAddressToIndexPlusOne[p.tokens[i]] = i + 1; unchecked {i++;} } @@ -252,7 +234,8 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool uint256 maxAmountIn, int128 limitPrice, uint256 deadline, - bool unwrap + bool unwrap, + bytes memory cbData ) external payable native nonReentrant killable returns (uint256 amountIn, uint256 amountOut, uint256 inFee) { require(deadline == 0 || block.timestamp <= deadline, "swap: deadline exceeded"); @@ -264,24 +247,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool IERC20 tokenIn = _tokens[inputTokenIndex]; IERC20 tokenOut = _tokens[outputTokenIndex]; - if (fundingSelector == Funding.APPROVALS) - // Regular ERC20 permit of the pool to move the tokens - _receiveTokenFrom(payer, tokenIn, totalTransferAmount); - else if (fundingSelector == Funding.PREFUNDING) { - require(limitPrice==0, 'Prefunding cannot be used with a limit price'); - uint256 balance = tokenIn.balanceOf(address(this)); - uint256 prevBalance = _cachedUintBalances[inputTokenIndex] + _protocolFeesOwed[inputTokenIndex]; - require( balance - prevBalance == totalTransferAmount, 'Incorrect prefunding amount'); - } - else { - // Callback-style funding mechanism - uint256 startingBalance = tokenIn.balanceOf(address(this)); - bytes memory data = abi.encodeWithSelector(fundingSelector, tokenIn, totalTransferAmount); - // Invoke the payer callback; no return value expected (reverts on failure) - Address.functionCall(payer, data); - uint256 endingBalance = tokenIn.balanceOf(address(this)); - require(endingBalance-startingBalance == totalTransferAmount, 'Insufficient funds'); - } + _receiveTokenFrom(payer, fundingSelector, inputTokenIndex, tokenIn, totalTransferAmount, limitPrice, cbData); // Compute on-chain balances as: onchain = cached + owed (+/- transfer) uint256 balIAfter = _cachedUintBalances[inputTokenIndex] + _protocolFeesOwed[inputTokenIndex] + totalTransferAmount; @@ -369,22 +335,26 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool /// @inheritdoc IPartyPool function swapToLimit( address payer, + bytes4 fundingSelector, address receiver, uint256 inputTokenIndex, uint256 outputTokenIndex, int128 limitPrice, uint256 deadline, - bool unwrap + bool unwrap, + bytes memory cbData ) external payable returns (uint256 amountInUsed, uint256 amountOut, uint256 inFee) { bytes memory data = abi.encodeWithSelector( PartyPoolSwapImpl.swapToLimit.selector, payer, + fundingSelector, receiver, inputTokenIndex, outputTokenIndex, limitPrice, deadline, unwrap, + cbData, _pairFeePpm(inputTokenIndex, outputTokenIndex), PROTOCOL_FEE_PPM ); diff --git a/src/PartyPoolBalancedPair.sol b/src/PartyPoolBalancedPair.sol index 8465f15..136dac0 100644 --- a/src/PartyPoolBalancedPair.sol +++ b/src/PartyPoolBalancedPair.sol @@ -10,23 +10,6 @@ import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol"; import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol"; contract PartyPoolBalancedPair is PartyPool { - constructor( - address owner_, - string memory name_, - string memory symbol_, - IERC20[] memory tokens_, - int128 kappa_, - uint256[] memory fees_, - uint256 flashFeePpm_, - uint256 protocolFeePpm_, // NEW: protocol share of fees (ppm) - address protocolFeeAddress_, // NEW: recipient for collected protocol tokens - NativeWrapper wrapperToken_, - PartyPoolSwapImpl swapMintImpl_, - PartyPoolMintImpl mintImpl_ - ) - PartyPool(owner_, name_, symbol_, tokens_, kappa_, fees_, flashFeePpm_, protocolFeePpm_, protocolFeeAddress_, wrapperToken_, swapMintImpl_, mintImpl_) - {} - function _swapAmountsForExactInput(uint256 i, uint256 j, int128 a, int128 limitPrice) internal virtual override view returns (int128 amountIn, int128 amountOut) { return LMSRStabilizedBalancedPair.swapAmountsForExactInput(_lmsr, i, j, a, limitPrice); diff --git a/src/PartyPoolBase.sol b/src/PartyPoolBase.sol index c629fa0..1c0b474 100644 --- a/src/PartyPoolBase.sol +++ b/src/PartyPoolBase.sol @@ -1,15 +1,17 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.30; +import "../lib/openzeppelin-contracts/contracts/utils/Address.sol"; +import "./Funding.sol"; import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol"; -import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; -import {ReentrancyGuard} from "../lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol"; import {ERC20Internal} from "./ERC20Internal.sol"; +import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {LMSRStabilized} from "./LMSRStabilized.sol"; import {NativeWrapper} from "./NativeWrapper.sol"; import {OwnableInternal} from "./OwnableInternal.sol"; import {PartyPoolHelpers} from "./PartyPoolHelpers.sol"; +import {ReentrancyGuard} from "../lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol"; +import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; /// @notice Abstract base contract that contains storage and internal helpers only. /// No external/public functions here. @@ -18,11 +20,8 @@ abstract contract PartyPoolBase is OwnableInternal, ERC20Internal, ReentrancyGua using LMSRStabilized for LMSRStabilized.State; using SafeERC20 for IERC20; - NativeWrapper internal immutable WRAPPER_TOKEN; - - constructor( NativeWrapper wrapper_ ) { - WRAPPER_TOKEN = wrapper_; - } + bytes32 internal _nonce; // used for callback validation + NativeWrapper internal immutable WRAPPER; /// @notice Per-asset swap fees in ppm. Fees are applied on input for swaps; see helpers for composition rules. uint256[] internal _fees; @@ -144,12 +143,41 @@ abstract contract PartyPoolBase is OwnableInternal, ERC20Internal, ReentrancyGua Token transfer helpers (includes autowrap) ---------------------- */ + function _receiveTokenFrom(address payer, bytes4 fundingSelector, uint256 tokenIndex, IERC20 token, uint256 amount, int128 limitPrice, bytes memory cbData) internal { + if (fundingSelector == Funding.APPROVAL) { + // Regular ERC20 permit of the pool to move the tokens + _receiveTokenFrom(payer, token, amount); + } + else if (fundingSelector == Funding.PREFUNDING) { + // Tokens are already deposited into the pool + require(limitPrice==0, 'Prefunding cannot be used with a limit price'); + if( token == WRAPPER && msg.value >= amount ) + WRAPPER.deposit{value:amount}(); + else { + uint256 balance = token.balanceOf(address(this)); + uint256 prevBalance = _cachedUintBalances[tokenIndex] + _protocolFeesOwed[tokenIndex]; + require( balance - prevBalance == amount, 'Incorrect prefunding amount'); + } + } + else { + // Callback-style funding mechanism + // Does not support native transfer. + uint256 startingBalance = token.balanceOf(address(this)); + bytes memory data = abi.encodeWithSelector(fundingSelector, _nonce, token, amount, cbData); + // Invoke the payer callback; no return value expected (reverts on failure) + Address.functionCall(payer, data); + uint256 endingBalance = token.balanceOf(address(this)); + require(endingBalance-startingBalance == amount, 'Insufficient funds'); + } + } + + /// @notice Receive _tokens from `payer` into the pool (address(this)) using SafeERC20 semantics. /// @dev Note: this helper does NOT query the on-chain balance after transfer to save gas. /// Callers should query the balance themselves when they need it (e.g., to detect fee-on-transfer _tokens). function _receiveTokenFrom(address payer, IERC20 token, uint256 amount) internal { - if( token == WRAPPER_TOKEN && msg.value >= amount ) - WRAPPER_TOKEN.deposit{value:amount}(); + if( token == WRAPPER && msg.value >= amount ) + WRAPPER.deposit{value:amount}(); else token.safeTransferFrom(payer, address(this), amount); } @@ -158,8 +186,8 @@ abstract contract PartyPoolBase is OwnableInternal, ERC20Internal, ReentrancyGua /// @dev Note: this helper does NOT query the on-chain balance after transfer to save gas. /// Callers should query the balance themselves when they need it (e.g., to detect fee-on-transfer _tokens). function _sendTokenTo(IERC20 token, address receiver, uint256 amount, bool unwrap) internal { - if( unwrap && token == WRAPPER_TOKEN ) { - WRAPPER_TOKEN.withdraw(amount); + if( unwrap && token == WRAPPER) { + WRAPPER.withdraw(amount); (bool ok, ) = receiver.call{value: amount}(""); require(ok, 'receiver not payable'); } diff --git a/src/PartyPoolDeployer.sol b/src/PartyPoolDeployer.sol index fc2eafc..0bd571b 100644 --- a/src/PartyPoolDeployer.sol +++ b/src/PartyPoolDeployer.sol @@ -1,91 +1,95 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.30; -import "./PartyPoolMintImpl.sol"; -import "./PartyPoolSwapImpl.sol"; +import {IPartyPool} from "./IPartyPool.sol"; +import {IPartyPoolDeployer} from "./IPartyPoolDeployer.sol"; import {PartyPool} from "./PartyPool.sol"; import {PartyPoolBalancedPair} from "./PartyPoolBalancedPair.sol"; -// This pattern is needed because the PartyPlanner constructs two different types of pools (regular and balanced-pair) -// but doesn't have room to store the initialization code of both contracts. Therefore, we delegate pool construction. - -interface IPartyPoolDeployer { - function deploy( - address owner_, - string memory name_, - string memory symbol_, - IERC20[] memory tokens_, - int128 kappa_, - uint256[] memory fees_, - uint256 flashFeePpm_, - uint256 protocolFeePpm_, - address protocolFeeAddress_, - NativeWrapper wrapper_, - PartyPoolSwapImpl swapImpl_, - PartyPoolMintImpl mintImpl_ - ) external returns (IPartyPool pool); +// Storage contracts that only hold the init code +contract PartyPoolInitCode { + constructor() { + bytes memory code = type(PartyPool).creationCode; + assembly { + return(add(code, 0x20), mload(code)) + } + } } +contract PartyPoolBalancedPairInitCode { + constructor() { + bytes memory code = type(PartyPoolBalancedPair).creationCode; + assembly { + return(add(code, 0x20), mload(code)) + } + } +} + +/// @notice Unified deployer that loads init code from external storage contracts +/// @dev This pattern avoids storing large init code in the deployer itself, reducing contract size. +/// Holds storage addresses for both regular and balanced pair pools, with separate nonce counters. contract PartyPoolDeployer is IPartyPoolDeployer { - function deploy( - address owner_, - string memory name_, - string memory symbol_, - IERC20[] memory tokens_, - int128 kappa_, - uint256[] memory fees_, - uint256 flashFeePpm_, - uint256 protocolFeePpm_, - address protocolFeeAddress_, - NativeWrapper wrapper_, - PartyPoolSwapImpl swapImpl_, - PartyPoolMintImpl mintImpl_ - ) external returns (IPartyPool) { - return new PartyPool( - owner_, - name_, - symbol_, - tokens_, - kappa_, - fees_, - flashFeePpm_, - protocolFeePpm_, - protocolFeeAddress_, - wrapper_, - swapImpl_, - mintImpl_ - ); - } -} + address private immutable POOL_INIT_CODE_STORAGE; + address private immutable BALANCED_PAIR_INIT_CODE_STORAGE; -contract PartyPoolBalancedPairDeployer is IPartyPoolDeployer { - function deploy( - address owner_, - string memory name_, - string memory symbol_, - IERC20[] memory tokens_, - int128 kappa_, - uint256[] memory fees_, - uint256 flashFeePpm_, - uint256 protocolFeePpm_, - address protocolFeeAddress_, - NativeWrapper wrapper_, - PartyPoolSwapImpl swapImpl_, - PartyPoolMintImpl mintImpl_ - ) external returns (IPartyPool) { - return new PartyPoolBalancedPair( - owner_, - name_, - symbol_, - tokens_, - kappa_, - fees_, - flashFeePpm_, - protocolFeePpm_, - protocolFeeAddress_, - wrapper_, - swapImpl_, - mintImpl_ - ); + uint256 private _poolNonce; + uint256 private _balancedPairNonce; + DeployParams private _params; + + constructor(PartyPoolInitCode poolInitCodeStorage, PartyPoolBalancedPairInitCode balancedPairInitCodeStorage) { + require(address(poolInitCodeStorage) != address(0), "Deployer: zero pool storage address"); + require(address(balancedPairInitCodeStorage) != address(0), "Deployer: zero balanced pair storage address"); + POOL_INIT_CODE_STORAGE = address(poolInitCodeStorage); + BALANCED_PAIR_INIT_CODE_STORAGE = address(balancedPairInitCodeStorage); + } + + function params() external view returns (DeployParams memory) { + return _params; + } + + /// @notice Deploy a regular PartyPool + function _deploy(DeployParams memory params_) internal returns (IPartyPool pool) { + return _doDeploy(params_, POOL_INIT_CODE_STORAGE, _poolNonce++); + } + + /// @notice Deploy a balanced pair PartyPool + function _deployBalancedPair(DeployParams memory params_) internal returns (IPartyPool pool) { + return _doDeploy(params_, BALANCED_PAIR_INIT_CODE_STORAGE, _balancedPairNonce++); + } + + /// @notice Internal deployment implementation shared by both pool types + function _doDeploy( + DeployParams memory params_, + address initCodeStorage, + uint256 nonce + ) internal returns (IPartyPool pool) { + bytes32 salt = bytes32(nonce); + _params = params_; + _params.nonce = salt; + + // Load init code from storage contract and deploy with CREATE2 + bytes memory initCode = _getInitCode(initCodeStorage); + address poolAddress; + assembly { + poolAddress := create2(0, add(initCode, 0x20), mload(initCode), salt) + if iszero(poolAddress) { + revert(0, 0) + } + } + + pool = IPartyPool(poolAddress); + } + + /// @notice Load init code from the specified storage contract using EXTCODECOPY + function _getInitCode(address storageContract) internal view returns (bytes memory) { + uint256 size; + assembly { + size := extcodesize(storageContract) + } + bytes memory code = new bytes(size); + assembly { + extcodecopy(storageContract, add(code, 0x20), 0, size) + } + return code; } } diff --git a/src/PartyPoolMintImpl.sol b/src/PartyPoolMintImpl.sol index 5acce5f..d009221 100644 --- a/src/PartyPoolMintImpl.sol +++ b/src/PartyPoolMintImpl.sol @@ -19,7 +19,7 @@ contract PartyPoolMintImpl is PartyPoolBase { using LMSRStabilized for LMSRStabilized.State; using SafeERC20 for IERC20; - constructor(NativeWrapper wrapper_) PartyPoolBase(wrapper_) {} + constructor(NativeWrapper wrapper_) {WRAPPER = wrapper_;} // // Initialization Mint diff --git a/src/PartyPoolSwapImpl.sol b/src/PartyPoolSwapImpl.sol index 15e074a..f26062a 100644 --- a/src/PartyPoolSwapImpl.sol +++ b/src/PartyPoolSwapImpl.sol @@ -18,7 +18,7 @@ contract PartyPoolSwapImpl is PartyPoolBase { using LMSRStabilized for LMSRStabilized.State; using SafeERC20 for IERC20; - constructor(NativeWrapper wrapper_) PartyPoolBase(wrapper_) {} + constructor(NativeWrapper wrapper_) {WRAPPER = wrapper_;} bytes32 internal constant FLASH_CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); @@ -94,12 +94,14 @@ contract PartyPoolSwapImpl is PartyPoolBase { function swapToLimit( address payer, + bytes4 fundingSelector, address receiver, uint256 inputTokenIndex, uint256 outputTokenIndex, int128 limitPrice, uint256 deadline, bool unwrap, + bytes memory cbData, uint256 swapFeePpm, uint256 protocolFeePpm ) external payable native killable nonReentrant returns (uint256 amountInUsed, uint256 amountOut, uint256 inFee) { @@ -109,18 +111,15 @@ contract PartyPoolSwapImpl is PartyPoolBase { require(deadline == 0 || block.timestamp <= deadline, "swapToLimit: deadline exceeded"); // Read previous balances for affected assets - uint256 prevBalI = IERC20(_tokens[inputTokenIndex]).balanceOf(address(this)); - uint256 prevBalJ = IERC20(_tokens[outputTokenIndex]).balanceOf(address(this)); + uint256 prevBalJ = _cachedUintBalances[outputTokenIndex]; // Compute amounts using the same path as views (uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalMax, int128 amountOutInternal, uint256 amountInUsedUint, uint256 feeUint) = _quoteSwapToLimit(inputTokenIndex, outputTokenIndex, limitPrice, swapFeePpm); - // Transfer the exact amount needed from payer and require exact receipt (revert on fee-on-transfer) + // Transfer the exact amount needed from payer IERC20 tokenIn = _tokens[inputTokenIndex]; - _receiveTokenFrom(payer, tokenIn, totalTransferAmount); - uint256 balIAfter = tokenIn.balanceOf(address(this)); - require(balIAfter == prevBalI + totalTransferAmount, "swapToLimit: non-standard tokenIn"); + _receiveTokenFrom(payer, fundingSelector, inputTokenIndex, tokenIn, totalTransferAmount, limitPrice, cbData); // Transfer output to receiver and verify exact decrease IERC20 tokenOut = _tokens[outputTokenIndex]; @@ -137,10 +136,6 @@ contract PartyPoolSwapImpl is PartyPoolBase { } } - // Update caches to effective balances (inline _recordCachedBalance) - require(balIAfter >= _protocolFeesOwed[inputTokenIndex], "balance < protocol owed"); - _cachedUintBalances[inputTokenIndex] = balIAfter - _protocolFeesOwed[inputTokenIndex]; - require(balJAfter >= _protocolFeesOwed[outputTokenIndex], "balance < protocol owed"); _cachedUintBalances[outputTokenIndex] = balJAfter - _protocolFeesOwed[outputTokenIndex]; diff --git a/src/PartySwapCallbackVerifier.sol b/src/PartySwapCallbackVerifier.sol new file mode 100644 index 0000000..bed9963 --- /dev/null +++ b/src/PartySwapCallbackVerifier.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.30; + +import {IPartyPlanner} from "./IPartyPlanner.sol"; +import {PartyPool} from "./PartyPool.sol"; +import {PartyPoolBalancedPair} from "./PartyPoolBalancedPair.sol"; + +library PartySwapCallbackVerifier { + + // To use this verification in your own library, run `forge script InitCodeHashes` and replace the computed hashes below with the hardcoded bytes32 hash + function verifyCallback(IPartyPlanner planner, bytes32 nonce) internal view { + if(_verify(planner, keccak256(type(PartyPool).creationCode), nonce)) return; + if(_verify(planner, keccak256(type(PartyPoolBalancedPair).creationCode), nonce)) return; + revert('unauthorized callback'); + } + + function _verify(IPartyPlanner planner, bytes32 initCodeHash, bytes32 nonce) internal view returns (bool) { + address predicted = address(uint160(uint256(keccak256(abi.encodePacked( + bytes1(0xff), + address(planner), + nonce, + initCodeHash + ))))); + return predicted == msg.sender; + } + +} diff --git a/test/Deploy.sol b/test/Deploy.sol index 60efb8c..1516d72 100644 --- a/test/Deploy.sol +++ b/test/Deploy.sol @@ -2,59 +2,61 @@ pragma solidity ^0.8.30; import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {IPartyInfo} from "../src/IPartyInfo.sol"; +import {IPartyPlanner} from "../src/IPartyPlanner.sol"; +import {IPartyPool} from "../src/IPartyPool.sol"; import {NativeWrapper} from "../src/NativeWrapper.sol"; +import {PartyInfo} from "../src/PartyInfo.sol"; import {PartyPlanner} from "../src/PartyPlanner.sol"; -import {PartyPool} from "../src/PartyPool.sol"; -import {PartyPoolBalancedPair} from "../src/PartyPoolBalancedPair.sol"; -import {PartyPoolDeployer, PartyPoolBalancedPairDeployer} from "../src/PartyPoolDeployer.sol"; +import {PartyPoolInitCode, PartyPoolBalancedPairInitCode} from "../src/PartyPoolDeployer.sol"; import {PartyPoolMintImpl} from "../src/PartyPoolMintImpl.sol"; import {PartyPoolSwapImpl} from "../src/PartyPoolSwapImpl.sol"; -import {PartyInfo} from "../src/PartyInfo.sol"; import {WETH9} from "./WETH9.sol"; +import {MockERC20} from "./MockERC20.sol"; library Deploy { address internal constant PROTOCOL_FEE_RECEIVER = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; // dev account #1 uint256 internal constant PROTOCOL_FEE_PPM = 100_000; // 10% - function newPartyPlanner() internal returns (PartyPlanner) { + function newPartyPlanner() internal returns (IPartyPlanner) { NativeWrapper wrapper = new WETH9(); - return newPartyPlanner(msg.sender, wrapper); + return newPartyPlanner(address(this), wrapper); } - function newPartyPlanner(address owner) internal returns (PartyPlanner) { + function newPartyPlanner(address owner) internal returns (IPartyPlanner) { NativeWrapper wrapper = new WETH9(); return newPartyPlanner(owner, wrapper); } - function newPartyPlanner(address owner, NativeWrapper wrapper) internal returns (PartyPlanner) { + function newPartyPlanner(address owner, NativeWrapper wrapper) internal returns (IPartyPlanner) { return new PartyPlanner( owner, wrapper, new PartyPoolSwapImpl(wrapper), new PartyPoolMintImpl(wrapper), - new PartyPoolDeployer(), - new PartyPoolBalancedPairDeployer(), + new PartyPoolInitCode(), + new PartyPoolBalancedPairInitCode(), PROTOCOL_FEE_PPM, PROTOCOL_FEE_RECEIVER ); } function newPartyPool( - address owner_, string memory name_, string memory symbol_, IERC20[] memory tokens_, int128 _kappa, uint256 _swapFeePpm, uint256 _flashFeePpm, - bool _stable - ) internal returns (PartyPool) { + bool _stable, + uint256 _initialBalance, + uint256 _lpTokens + ) internal returns (IPartyPool pool) { NativeWrapper wrapper = new WETH9(); - return newPartyPool(owner_, name_, symbol_, tokens_, _kappa, _swapFeePpm, _flashFeePpm, wrapper, _stable); + (pool,) = newPartyPool2(NPPArgs(name_, symbol_, tokens_, _kappa, _swapFeePpm, _flashFeePpm, wrapper, _stable, _initialBalance, _lpTokens)); } function newPartyPool( - address owner_, string memory name_, string memory symbol_, IERC20[] memory tokens_, @@ -62,44 +64,92 @@ library Deploy { uint256 _swapFeePpm, uint256 _flashFeePpm, NativeWrapper wrapper, - bool _stable - ) internal returns (PartyPool) { + bool _stable, + uint256 _initialBalance, + uint256 _lpTokens + ) internal returns (IPartyPool pool) { + (pool,) = newPartyPool2(NPPArgs(name_, symbol_, tokens_, _kappa, _swapFeePpm, _flashFeePpm, wrapper, _stable, _initialBalance, _lpTokens)); + } + + + function newPartyPool2( + string memory name_, + string memory symbol_, + IERC20[] memory tokens_, + int128 _kappa, + uint256 _swapFeePpm, + uint256 _flashFeePpm, + bool _stable, + uint256 _initialBalance, + uint256 _lpTokens + ) internal returns (IPartyPool pool, uint256 lpTokens) { + NativeWrapper wrapper = new WETH9(); + return newPartyPool2(NPPArgs(name_, symbol_, tokens_, _kappa, _swapFeePpm, _flashFeePpm, wrapper, _stable, _initialBalance, _lpTokens)); + } + + struct NPPVars { + address planner; + uint256[] feesArr; + uint256[] deposits; + } + + struct NPPArgs { + string name; + string symbol; + IERC20[] tokens; + int128 kappa; + uint256 swapFeePpm; + uint256 flashFeePpm; + NativeWrapper wrapper; + bool stable; + uint256 initialBalance; + uint256 lpTokens; + } + + function newPartyPool2( NPPArgs memory args ) internal returns (IPartyPool pool, uint256 lpTokens) { + NPPVars memory v = NPPVars( + address(newPartyPlanner(address(this), args.wrapper)), + new uint256[](args.tokens.length), + new uint256[](args.tokens.length) + ); + address self = address(this); + // Build per-asset fee vector from scalar for tests - uint256[] memory feesArr = new uint256[](tokens_.length); - for (uint256 i = 0; i < tokens_.length; i++) { feesArr[i] = _swapFeePpm; } - return _stable && tokens_.length == 2 ? - new PartyPoolBalancedPair( - owner_, - name_, - symbol_, - tokens_, - _kappa, - feesArr, - _flashFeePpm, - PROTOCOL_FEE_PPM, - PROTOCOL_FEE_RECEIVER, - wrapper, - new PartyPoolSwapImpl(wrapper), - new PartyPoolMintImpl(wrapper) - ) : - new PartyPool( - owner_, - name_, - symbol_, - tokens_, - _kappa, - feesArr, - _flashFeePpm, - PROTOCOL_FEE_PPM, - PROTOCOL_FEE_RECEIVER, - wrapper, - new PartyPoolSwapImpl(wrapper), - new PartyPoolMintImpl(wrapper) + for (uint256 i = 0; i < args.tokens.length; i++) { v.feesArr[i] = args.swapFeePpm; } + + for (uint256 i = 0; i < args.tokens.length; i++) { + if (address(args.tokens[i]) == address(args.wrapper)) { + // Not a MockERC20. Wrap coins instead of minting. + args.wrapper.deposit{value: args.initialBalance}(); + args.wrapper.approve(v.planner, args.initialBalance); + v.deposits[i] = args.initialBalance; + } + else { + MockERC20 t = MockERC20(address(args.tokens[i])); + t.mint(self, args.initialBalance); + t.approve(v.planner, args.initialBalance); + v.deposits[i] = args.initialBalance; + } + } + + (pool, lpTokens) = IPartyPlanner(v.planner).newPool( + args.name, + args.symbol, + args.tokens, + args.kappa, + args.swapFeePpm, + args.flashFeePpm, + args.stable, + self, + self, + v.deposits, + args.lpTokens, + 0 ); } - function newInfo() internal returns (PartyInfo) { + function newInfo() internal returns (IPartyInfo) { NativeWrapper wrapper = new WETH9(); return new PartyInfo(new PartyPoolSwapImpl(wrapper), new PartyPoolMintImpl(wrapper)); } diff --git a/test/FundingSwapTest.sol b/test/FundingSwapTest.sol index 1d9e6d9..ff3e3ac 100644 --- a/test/FundingSwapTest.sol +++ b/test/FundingSwapTest.sol @@ -4,14 +4,23 @@ pragma solidity ^0.8.30; 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"; +import {StdChains} from "../lib/forge-std/src/StdChains.sol"; +import {StdCheats, StdCheatsSafe} from "../lib/forge-std/src/StdCheats.sol"; +import {StdUtils} from "../lib/forge-std/src/StdUtils.sol"; import {Test} from "../lib/forge-std/src/Test.sol"; -import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {ERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {Funding} from "../src/Funding.sol"; +import {IPartyInfo} from "../src/IPartyInfo.sol"; +import {IPartyPlanner} from "../src/IPartyPlanner.sol"; +import {IPartyPool} from "../src/IPartyPool.sol"; import {LMSRStabilized} from "../src/LMSRStabilized.sol"; -import {PartyInfo} from "../src/PartyInfo.sol"; -import {PartyPool} from "../src/PartyPool.sol"; +import {PartyPoolDeployer} from "../src/PartyPoolDeployer.sol"; +import {PartySwapCallbackVerifier} from "../src/PartySwapCallbackVerifier.sol"; import {Deploy} from "./Deploy.sol"; +import {TestERC20, SwapCallbackContract} from "./FundingSwapTest.sol"; /// @notice Minimal ERC20 token for tests with an external mint function. contract TestERC20 is ERC20 { @@ -37,9 +46,11 @@ contract SwapCallbackContract { address public pool; address public tokenSource; bool public shouldFail; + IPartyPlanner public planner; - constructor(address _pool) { + constructor(address _pool, IPartyPlanner _planner) { pool = _pool; + planner = _planner; } function setTokenSource(address _tokenSource) external { @@ -51,16 +62,17 @@ contract SwapCallbackContract { } /// @notice Called by PartyPool.swap on the payer. Signature must be: - /// provideFunding(address token, uint256 amount) + /// provideFunding(bytes32 nonce, IERC20 inputToken, uint256 amount, bytes memory data) /// @dev The pool will call this function to request the input token; this function /// pulls funds from tokenSource (via ERC20.transferFrom) into the pool. - function provideFunding(address token, uint256 amount) external { + function provideFunding(bytes32 nonce, IERC20 token, uint256 amount, bytes memory) external { + PartySwapCallbackVerifier.verifyCallback(planner, nonce); require(msg.sender == pool, "Callback not called by pool"); if (shouldFail) revert("callback failed"); require(tokenSource != address(0), "no token source"); // Pull the required tokens from tokenSource into the pool - TestERC20(token).transferFrom(tokenSource, pool, amount); + token.transferFrom(tokenSource, pool, amount); } } @@ -73,9 +85,10 @@ contract FundingTest is Test { TestERC20 token0; TestERC20 token1; TestERC20 token2; - PartyPool pool; - PartyPool poolZeroFee; - PartyInfo info; + IPartyPlanner planner; + IPartyPool pool; + IPartyPool poolZeroFee; + IPartyInfo info; SwapCallbackContract callbackContract; address alice; @@ -117,31 +130,26 @@ contract FundingTest is Test { // Deploy pool with a small fee (0.1%) uint256 feePpm = 1000; int128 kappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); - pool = Deploy.newPartyPool(address(this), "LP", "LP", tokens, kappa, feePpm, feePpm, false); - // Transfer initial deposit amounts into pool - token0.transfer(address(pool), INIT_BAL); - token1.transfer(address(pool), INIT_BAL); - token2.transfer(address(pool), INIT_BAL); + planner = Deploy.newPartyPlanner(); + uint256[] memory deposits = new uint256[](tokens.length); + for(uint256 i=0; i 0.01, slippage 0.001 tradeFrac = ABDKMath64x64.divu(100, 10_000); // 0.01 targetSlippage = ABDKMath64x64.divu(10, 10_000); // 0.001 @@ -238,39 +288,35 @@ contract GasTest is Test { /// @notice Helper function: perform 10 swaps back-and-forth between the first two _tokens. function _performSwapGasTest(IPartyPool testPool) internal { - _performSwapGasTest(testPool, Funding.APPROVALS); + _performSwapGasTest(testPool, Funding.APPROVAL); } - function sendTokensCallback(IERC20 token, uint256 amount) external { - // verify the caller - require(planner.getPoolSupported(msg.sender), 'Not a LiqP pool'); - token.transferFrom( alice, msg.sender, amount); + + function _doSwap( + IPartyPool pool, + address payer, + bytes4 fundingSelector, + address receiver, + uint256 inputTokenIndex, + uint256 outputTokenIndex, + uint256 maxAmountIn, + int128 limitPrice, + uint256 deadline, + bool unwrap + ) internal returns (uint256 amountIn, uint256 amountOut, uint256 inFee) { + if (fundingSelector == Funding.APPROVAL) + return harness.swapApproval{value:msg.value}(pool, pool.token(inputTokenIndex), payer, fundingSelector, receiver, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, deadline, unwrap); + if (fundingSelector == Funding.PREFUNDING) { + pool.token(inputTokenIndex).transfer(address(harness), maxAmountIn); + return harness.swapPrefund{value:msg.value}(pool, payer, fundingSelector, receiver, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, deadline, unwrap); + } + else + return harness.swapCallback{value:msg.value}(pool, payer, fundingSelector, receiver, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, deadline, unwrap); } function _performSwapGasTest(IPartyPool testPool, bytes4 fundingSelector) internal { IERC20[] memory tokens = testPool.allTokens(); require(tokens.length >= 2, "Pool must have at least 2 tokens"); - address payer; - address spender; - - if (fundingSelector == Funding.PREFUNDING) { - payer = address(this); - spender = address(this); - } - else if (fundingSelector == Funding.APPROVALS) { - payer = alice; - spender = address(testPool); - } - else { - payer = address(this); - spender = address(this); - } - TestERC20 token0 = TestERC20(address(tokens[0])); - TestERC20 token1 = TestERC20(address(tokens[1])); - vm.prank(alice); - token0.approve(spender, type(uint256).max); - vm.prank(alice); - token1.approve(spender, type(uint256).max); uint256 maxIn = 10_000; @@ -278,15 +324,11 @@ contract GasTest is Test { vm.startPrank(alice); for (uint256 i = 0; i < 20; i++) { if (i % 2 == 0) { - if (fundingSelector == Funding.PREFUNDING) - token0.transfer(address(testPool), maxIn); // swap token0 -> token1 - testPool.swap(payer, fundingSelector, alice, 0, 1, maxIn, 0, 0, false); + _doSwap(testPool, alice, fundingSelector, alice, 0, 1, maxIn, 0, 0, false); } else { // swap token1 -> token0 - if (fundingSelector == Funding.PREFUNDING) - token1.transfer(address(testPool), maxIn); - testPool.swap(payer, fundingSelector, alice, 1, 0, maxIn, 0, 0, false); + _doSwap( testPool, alice, fundingSelector, alice, 1, 0, maxIn, 0, 0, false); } // shake up the bits maxIn *= 787; @@ -306,20 +348,29 @@ contract GasTest is Test { } /// @notice Gas measurement: perform 10 swaps back-and-forth between first two _tokens in the 10-token pool using the callback funding method. - function testSwapGasCallback() public { - _performSwapGasTest(pool10, this.sendTokensCallback.selector); + function testSwapGasCallback10() public { + _performSwapGasTest(pool10, IPartySwapCallback.liquidityPartySwapCallback.selector); } /// @notice Gas measurement: perform 10 swaps back-and-forth between first two _tokens in the 10-token pool using the callback funding method. - function testSwapGasPrefunding() public { + function testSwapGasPrefunding10() public { _performSwapGasTest(pool10, Funding.PREFUNDING); } + function testSwapGasPrefunding20() public { + _performSwapGasTest(pool20, Funding.PREFUNDING); + } + /// @notice Gas measurement: perform 10 swaps back-and-forth between first two _tokens in the 20-token pool. function testSwapGasTwenty() public { _performSwapGasTest(pool20); } + /// @notice Gas measurement: perform 10 swaps back-and-forth between first two _tokens in the 10-token pool using the callback funding method. + function testSwapGasCallback20() public { + _performSwapGasTest(pool20, IPartySwapCallback.liquidityPartySwapCallback.selector); + } + /// @notice Gas measurement: perform 10 swaps back-and-forth between first two _tokens in the 100-token pool. function testSwapGasFifty() public { _performSwapGasTest(pool50); @@ -331,6 +382,12 @@ contract GasTest is Test { _performSwapGasTest(stablePair); } + /// @notice Gas measurement: perform 10 swaps back-and-forth on a 2-token stable pair (stable-path enabled) + function testSwapGasPrefundingSP() public { + IPartyPool stablePair = createPoolStable(2); + _performSwapGasTest(stablePair, IPartySwapCallback.liquidityPartySwapCallback.selector); + } + /// @notice Gas-style test: alternate swapMint then burnSwap on a 2-token stable pair function testSwapMintBurnSwapGasStablePair() public { IPartyPool stablePair = createPoolStable(2); diff --git a/test/NativeTest.t.sol b/test/NativeTest.t.sol index 9849624..f4a6f78 100644 --- a/test/NativeTest.t.sol +++ b/test/NativeTest.t.sol @@ -12,9 +12,9 @@ import {Test} from "../lib/forge-std/src/Test.sol"; import {ERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {Funding} from "../src/Funding.sol"; +import {IPartyInfo} from "../src/IPartyInfo.sol"; +import {IPartyPool} from "../src/IPartyPool.sol"; import {LMSRStabilized} from "../src/LMSRStabilized.sol"; -import {PartyInfo} from "../src/PartyInfo.sol"; -import {PartyPool} from "../src/PartyPool.sol"; import {Deploy} from "./Deploy.sol"; import {TestERC20Native} from "./NativeTest.t.sol"; import {WETH9} from "./WETH9.sol"; @@ -46,8 +46,8 @@ contract NativeTest is Test { TestERC20Native token0; TestERC20Native token1; WETH9 weth; // WETH is our third token - PartyPool pool; - PartyInfo info; + IPartyPool pool; + IPartyInfo info; address alice; address bob; @@ -78,9 +78,6 @@ contract NativeTest is Test { token0.mint(address(this), INIT_BAL); token1.mint(address(this), INIT_BAL); - // For WETH, we deposit native currency to get wrapped _tokens - weth.deposit{value: INIT_BAL}(); - // Configure LMSR parameters tradeFrac = ABDKMath64x64.divu(100, 10_000); // 0.01 targetSlippage = ABDKMath64x64.divu(10, 10_000); // 0.001 @@ -100,15 +97,7 @@ contract NativeTest is Test { uint256 feePpm = 1000; int128 kappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); - pool = Deploy.newPartyPool(address(this), "LP", "LP", tokens, kappa, feePpm, feePpm, weth, false); - - // Transfer initial deposit amounts into pool - token0.transfer(address(pool), INIT_BAL); - token1.transfer(address(pool), INIT_BAL); - weth.transfer(address(pool), INIT_BAL); - - // Perform initial mint - pool.initialMint(address(this), 0); + pool = Deploy.newPartyPool("LP", "LP", tokens, kappa, feePpm, feePpm, weth, false, INIT_BAL, 0); // Mint _tokens to alice and bob for testing token0.mint(alice, INIT_BAL); @@ -148,14 +137,15 @@ contract NativeTest is Test { // Send native currency with {value: maxIn} (uint256 amountIn, uint256 amountOut, ) = pool.swap{value: maxIn}( alice, // payer - Funding.APPROVALS, + Funding.APPROVAL, alice, // receiver 2, // inputTokenIndex (WETH) 0, // outputTokenIndex (token0) maxIn, // maxAmountIn 0, // limitPrice 0, // deadline - false // unwrap (output is not WETH, so false) + false, // unwrap (output is not WETH, so false) + '' ); // Verify amounts @@ -186,14 +176,15 @@ contract NativeTest is Test { // Execute swap: token0 (index 0) -> WETH (index 2) with unwrap=true (uint256 amountIn, uint256 amountOut, ) = pool.swap( alice, // payer - Funding.APPROVALS, // no selector: use ERC20 approvals + Funding.APPROVAL, // no selector: use ERC20 approvals alice, // receiver 0, // inputTokenIndex (token0) 2, // outputTokenIndex (WETH) maxIn, // maxAmountIn 0, // limitPrice 0, // deadline - true // unwrap (receive native currency instead of WETH) + true, // unwrap (receive native currency instead of WETH) + '' ); // Verify amounts @@ -222,14 +213,15 @@ contract NativeTest is Test { // Execute swap with excess native currency (uint256 amountIn, , ) = pool.swap{value: totalSent}( alice, // payer - Funding.APPROVALS, + Funding.APPROVAL, alice, // receiver 2, // inputTokenIndex (WETH) 0, // outputTokenIndex (token0) maxIn, // maxAmountIn 0, // limitPrice 0, // deadline - false // unwrap + false, // unwrap + '' ); // Verify that only amountIn was used, and excess was refunded @@ -253,12 +245,14 @@ contract NativeTest is Test { uint256 largeAmount = 100_000; (uint256 amountInUsed, uint256 amountOut, uint256 fee) = pool.swapToLimit{value: largeAmount}( alice, // payer + Funding.APPROVAL, alice, // receiver 2, // inputTokenIndex (WETH) 0, // outputTokenIndex (token0) limitPrice, // limitPrice 0, // deadline - false // unwrap + false, // unwrap + '' ); assertTrue(amountInUsed > 0, "expected some input used for swapToLimit"); @@ -283,12 +277,14 @@ contract NativeTest is Test { // Execute swapToLimit: token0 (index 0) -> WETH (index 2) with unwrap=true (uint256 amountInUsed, uint256 amountOut, /*uint256 fee*/) = pool.swapToLimit( alice, // payer + Funding.APPROVAL, alice, // receiver 0, // inputTokenIndex (token0) 2, // outputTokenIndex (WETH) limitPrice, // limitPrice 0, // deadline - true // unwrap (receive native currency) + true, // unwrap (receive native currency) + '' ); assertTrue(amountInUsed > 0, "expected some input used"); @@ -551,14 +547,14 @@ contract NativeTest is Test { // 2. Swap native currency for token0 uint256 swapAmount = 5_000; (, uint256 amountOut, ) = pool.swap{value: swapAmount}( - alice,Funding.APPROVALS,alice, 2, 0, swapAmount, 0, 0, false + alice,Funding.APPROVAL,alice, 2, 0, swapAmount, 0, 0, false, '' ); assertTrue(amountOut > 0, "Should receive token0"); // 3. Swap token0 back to native currency uint256 token0Balance = token0.balanceOf(alice); (, uint256 swapOut2, ) = pool.swap( - alice, Funding.APPROVALS, alice, 0, 2, token0Balance / 2, 0, 0, true + alice, Funding.APPROVAL, alice, 0, 2, token0Balance / 2, 0, 0, true, '' ); assertTrue(swapOut2 > 0, "Should receive native currency"); @@ -585,7 +581,7 @@ contract NativeTest is Test { // Swap token0 -> WETH without unwrap (, uint256 amountOut, ) = pool.swap( - alice, Funding.APPROVALS, alice, 0, 2, maxIn, 0, 0, false // unwrap=false + alice, Funding.APPROVAL, alice, 0, 2, maxIn, 0, 0, false, '' ); assertTrue(amountOut > 0, "Should receive WETH tokens"); @@ -606,7 +602,7 @@ contract NativeTest is Test { // Try to swap token0 (not WETH) by sending native currency - should revert vm.expectRevert(); pool.swap{value: 10_000}( - alice, Funding.APPROVALS, alice, 0, 1, 10_000, 0, 0, false + alice, Funding.APPROVAL, alice, 0, 1, 10_000, 0, 0, false, '' ); vm.stopPrank(); diff --git a/test/PartyPlanner.t.sol b/test/PartyPlanner.t.sol index 50f8cd7..963a458 100644 --- a/test/PartyPlanner.t.sol +++ b/test/PartyPlanner.t.sol @@ -9,11 +9,10 @@ import {StdUtils} from "../lib/forge-std/src/StdUtils.sol"; import {Test} from "../lib/forge-std/src/Test.sol"; import {ERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import {Deploy} from "./Deploy.sol"; +import {IPartyPlanner} from "../src/IPartyPlanner.sol"; import {IPartyPool} from "../src/IPartyPool.sol"; import {LMSRStabilized} from "../src/LMSRStabilized.sol"; -import {PartyPlanner} from "../src/PartyPlanner.sol"; -import {PartyPool} from "../src/PartyPool.sol"; +import {Deploy} from "./Deploy.sol"; import {MockERC20} from "./PartyPlanner.t.sol"; // Mock ERC20 token for testing @@ -34,7 +33,7 @@ contract MockERC20 is ERC20 { } contract PartyPlannerTest is Test { - PartyPlanner public planner; + IPartyPlanner public planner; MockERC20 public tokenA; MockERC20 public tokenB; MockERC20 public tokenC; diff --git a/test/PartyPool.t.sol b/test/PartyPool.t.sol index 6b0e9cb..2de539b 100644 --- a/test/PartyPool.t.sol +++ b/test/PartyPool.t.sol @@ -13,9 +13,10 @@ import {IERC3156FlashBorrower} from "../lib/openzeppelin-contracts/contracts/int import {ERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {Funding} from "../src/Funding.sol"; +import {IPartyInfo} from "../src/IPartyInfo.sol"; +import {IPartyPlanner} from "../src/IPartyPlanner.sol"; +import {IPartyPool} from "../src/IPartyPool.sol"; import {LMSRStabilized} from "../src/LMSRStabilized.sol"; -import {PartyInfo} from "../src/PartyInfo.sol"; -import {PartyPlanner} from "../src/PartyPlanner.sol"; import {PartyPool} from "../src/PartyPool.sol"; import {Deploy} from "./Deploy.sol"; import {TestERC20, FlashBorrower} from "./PartyPool.t.sol"; @@ -118,10 +119,10 @@ contract PartyPoolTest is Test { TestERC20 token7; TestERC20 token8; TestERC20 token9; - PartyPlanner planner; - PartyPool pool; - PartyPool pool10; - PartyInfo info; + IPartyPlanner planner; + IPartyPool pool; + IPartyPool pool10; + IPartyInfo info; address alice; address bob; @@ -175,16 +176,8 @@ contract PartyPoolTest is Test { uint256 feePpm = 1000; int128 kappa3 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); - pool = Deploy.newPartyPool(address(this), "LP", "LP", tokens, kappa3, feePpm, feePpm, false); - - // Transfer initial deposit amounts into pool before initial mint (pool expects _tokens already in contract) - // We deposit equal amounts INIT_BAL for each token - token0.transfer(address(pool), INIT_BAL); - token1.transfer(address(pool), INIT_BAL); - token2.transfer(address(pool), INIT_BAL); - - // Perform initial mint (initial deposit); receiver is this contract - pool.initialMint(address(this), INIT_BAL * tokens.length * 10**18); + uint256 lpTokens = INIT_BAL * tokens.length * 10**18; + pool = Deploy.newPartyPool("LP", "LP", tokens, kappa3, feePpm, feePpm, false, INIT_BAL, lpTokens); // Set up pool10 with 10 _tokens IERC20[] memory tokens10 = new IERC20[](10); @@ -200,34 +193,7 @@ contract PartyPoolTest is Test { tokens10[9] = IERC20(address(token9)); int128 kappa10 = LMSRStabilized.computeKappaFromSlippage(tokens10.length, tradeFrac, targetSlippage); - pool10 = Deploy.newPartyPool(address(this), "LP10", "LP10", tokens10, kappa10, feePpm, feePpm, false); - - // Mint additional _tokens for pool10 initial deposit - token0.mint(address(this), INIT_BAL); - token1.mint(address(this), INIT_BAL); - token2.mint(address(this), INIT_BAL); - token3.mint(address(this), INIT_BAL); - token4.mint(address(this), INIT_BAL); - token5.mint(address(this), INIT_BAL); - token6.mint(address(this), INIT_BAL); - token7.mint(address(this), INIT_BAL); - token8.mint(address(this), INIT_BAL); - token9.mint(address(this), INIT_BAL); - - // Transfer initial deposit amounts into pool10 - token0.transfer(address(pool10), INIT_BAL); - token1.transfer(address(pool10), INIT_BAL); - token2.transfer(address(pool10), INIT_BAL); - token3.transfer(address(pool10), INIT_BAL); - token4.transfer(address(pool10), INIT_BAL); - token5.transfer(address(pool10), INIT_BAL); - token6.transfer(address(pool10), INIT_BAL); - token7.transfer(address(pool10), INIT_BAL); - token8.transfer(address(pool10), INIT_BAL); - token9.transfer(address(pool10), INIT_BAL); - - // Perform initial mint for pool10 - pool10.initialMint(address(this), 0); + pool10 = Deploy.newPartyPool("LP10", "LP10", tokens10, kappa10, feePpm, feePpm, false, INIT_BAL, 0); // For later tests we will mint _tokens to alice/bob as needed token0.mint(alice, INIT_BAL); @@ -431,7 +397,7 @@ contract PartyPoolTest is Test { // Execute swap: token0 -> token1 vm.prank(alice); - (uint256 amountInUsed, uint256 amountOut, uint256 fee) = pool.swap(alice, Funding.APPROVALS, bob, 0, 1, maxIn, 0, 0, false); + (uint256 amountInUsed, uint256 amountOut, uint256 fee) = pool.swap(alice, Funding.APPROVAL, bob, 0, 1, maxIn, 0, 0, false, ''); // Amounts should be positive and not exceed provided max assertTrue(amountInUsed > 0, "expected some input used"); @@ -460,7 +426,7 @@ contract PartyPoolTest is Test { vm.prank(alice); vm.expectRevert(bytes("LMSR: limitPrice <= current price")); - pool.swap(alice, Funding.APPROVALS, alice, 0, 1, 1000, limitPrice, 0, false); + pool.swap(alice, Funding.APPROVAL, alice, 0, 1, 1000, limitPrice, 0, false, ''); } /// @notice swapToLimit should compute input needed to reach a slightly higher price and execute. @@ -472,7 +438,7 @@ contract PartyPoolTest is Test { token0.approve(address(pool), type(uint256).max); vm.prank(alice); - (uint256 amountInUsed, uint256 amountOut, uint256 fee) = pool.swapToLimit(alice, bob, 0, 1, limitPrice, 0, false); + (uint256 amountInUsed, uint256 amountOut, uint256 fee) = pool.swapToLimit(alice, Funding.APPROVAL, bob, 0, 1, limitPrice, 0, false, ''); assertTrue(amountInUsed > 0, "expected some input used for swapToLimit"); assertTrue(amountOut > 0, "expected some output for swapToLimit"); @@ -984,32 +950,12 @@ contract PartyPoolTest is Test { // Pool with default initialization (lpTokens = 0) int128 kappaDefault = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); - PartyPool poolDefault = Deploy.newPartyPool(address(this), "LP_DEFAULT", "LP_DEFAULT", tokens, kappaDefault, feePpm, feePpm, false); + (IPartyPool poolDefault, uint256 lpDefault) = Deploy.newPartyPool2("LP_DEFAULT", "LP_DEFAULT", tokens, kappaDefault, feePpm, feePpm, false, INIT_BAL, 0); // Pool with custom initialization (lpTokens = custom amount) int128 kappaCustom = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); - PartyPool poolCustom = Deploy.newPartyPool(address(this), "LP_CUSTOM", "LP_CUSTOM", tokens, kappaCustom, 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); + (IPartyPool poolCustom, uint256 lpCustom) = Deploy.newPartyPool2("LP_CUSTOM", "LP_CUSTOM", tokens, kappaCustom, feePpm, feePpm, false, INIT_BAL, customLpAmount); // Verify the custom pool has the expected LP supply assertEq(lpCustom, customLpAmount, "Custom pool should have expected LP amount"); @@ -1032,8 +978,8 @@ contract PartyPoolTest is Test { token0.approve(address(poolCustom), type(uint256).max); // Perform identical swaps: token0 -> token1 - (uint256 amountInDefault, uint256 amountOutDefault, uint256 feeDefault) = poolDefault.swap(alice, Funding.APPROVALS, alice, 0, 1, swapAmount, 0, 0, false); - (uint256 amountInCustom, uint256 amountOutCustom, uint256 feeCustom) = poolCustom.swap(alice, Funding.APPROVALS, alice, 0, 1, swapAmount, 0, 0, false); + (uint256 amountInDefault, uint256 amountOutDefault, uint256 feeDefault) = poolDefault.swap(alice, Funding.APPROVAL, alice, 0, 1, swapAmount, 0, 0, false, ''); + (uint256 amountInCustom, uint256 amountOutCustom, uint256 feeCustom) = poolCustom.swap(alice, Funding.APPROVAL, alice, 0, 1, swapAmount, 0, 0, false, ''); // Swap results should be identical assertEq(amountInDefault, amountInCustom, "Swap input amounts should be identical"); @@ -1055,29 +1001,11 @@ contract PartyPoolTest is Test { uint256 feePpm = 1000; int128 kappaDefault2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); - PartyPool poolDefault = Deploy.newPartyPool(address(this), "LP_DEFAULT", "LP_DEFAULT", tokens, kappaDefault2, feePpm, feePpm, false); - int128 kappaCustom2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); - PartyPool poolCustom = Deploy.newPartyPool(address(this), "LP_CUSTOM", "LP_CUSTOM", tokens, kappaCustom2, 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); + (IPartyPool poolDefault, uint256 lpDefault) = Deploy.newPartyPool2("LP_DEFAULT", "LP_DEFAULT", tokens, kappaDefault2, feePpm, feePpm, false, INIT_BAL, 0); uint256 scaleFactor = 3; uint256 customLpAmount = lpDefault * scaleFactor; - poolCustom.initialMint(address(this), customLpAmount); + int128 kappaCustom2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); + (IPartyPool poolCustom,) = Deploy.newPartyPool2("LP_CUSTOM", "LP_CUSTOM", tokens, kappaCustom2, feePpm, feePpm, false, INIT_BAL, customLpAmount); // Verify initial LP supplies assertEq(poolDefault.totalSupply(), lpDefault, "Default pool should have default LP supply");