CREATE2 callback validation; init code storage contracts

This commit is contained in:
tim
2025-11-13 16:41:52 -04:00
parent c2ac0e3624
commit 9273430f2a
28 changed files with 779 additions and 588 deletions

View File

@@ -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

View File

@@ -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]

View File

@@ -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);

View File

@@ -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();

16
script/InitCodeHashes.sol Normal file
View File

@@ -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));
}
}

View File

@@ -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_;
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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.

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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));
}

View File

@@ -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<nAssets; i++ )
cachedUintBalances[i] = pool.getToken(i).balanceOf(address(pool));
cachedUintBalances[i] = pool.token(i).balanceOf(address(pool));
return MINT_IMPL.mintAmounts(lpTokenAmount, pool.totalSupply(), cachedUintBalances);
}
@@ -86,7 +86,7 @@ contract PartyInfo is PartyPoolHelpers, IPartyInfo {
uint256 nAssets = lmsr.qInternal.length;
uint256[] memory cachedUintBalances = new uint256[](nAssets);
for( uint256 i=0; i<nAssets; i++ )
cachedUintBalances[i] = pool.getToken(i).balanceOf(address(pool));
cachedUintBalances[i] = pool.token(i).balanceOf(address(pool));
return MINT_IMPL.burnAmounts(lpTokenAmount, pool.totalSupply(), cachedUintBalances);
}

View File

@@ -5,17 +5,19 @@ import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20
import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {IPartyPlanner} from "./IPartyPlanner.sol";
import {IPartyPool} from "./IPartyPool.sol";
import {IPartyPoolDeployer} from "./IPartyPoolDeployer.sol";
import {LMSRStabilized} from "./LMSRStabilized.sol";
import {NativeWrapper} from "./NativeWrapper.sol";
import {OwnableExternal} from "./OwnableExternal.sol";
import {OwnableInternal} from "./OwnableInternal.sol";
import {IPartyPoolDeployer} from "./PartyPoolDeployer.sol";
import {PartyPoolDeployer, PartyPoolInitCode, PartyPoolBalancedPairInitCode} from "./PartyPoolDeployer.sol";
import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol";
import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol";
/// @title PartyPlanner
/// @notice Factory contract for creating and tracking PartyPool instances
contract PartyPlanner is OwnableExternal, IPartyPlanner {
/// @dev Inherits from PartyPoolDeployer to handle pool deployment directly
contract PartyPlanner is PartyPoolDeployer, OwnableExternal, IPartyPlanner {
using SafeERC20 for IERC20;
int128 private constant ONE = int128(1) << 64;
@@ -38,9 +40,6 @@ contract PartyPlanner is OwnableExternal, IPartyPlanner {
NativeWrapper private immutable WRAPPER;
function wrapper() external view returns (NativeWrapper) { return WRAPPER; }
IPartyPoolDeployer private immutable NORMAL_POOL_DEPLOYER;
IPartyPoolDeployer private immutable BALANCED_PAIR_DEPLOYER;
// On-chain pool indexing
IPartyPool[] private _allPools;
IERC20[] private _allTokens;
@@ -52,6 +51,8 @@ contract PartyPlanner is OwnableExternal, IPartyPlanner {
/// @param wrapper_ The WETH9 implementation address used for this chain
/// @param swapImpl_ address of the Swap implementation contract to be used by all pools
/// @param mintImpl_ address of the Mint implementation contract to be used by all pools
/// @param poolInitCodeStorage_ address of the storage contract holding PartyPool init code
/// @param balancedPairInitCodeStorage_ address of the storage contract holding PartyPoolBalancedPair init code
/// @param protocolFeePpm_ protocol fee share (ppm) to be used for pools created by this planner
/// @param protocolFeeAddress_ recipient address for protocol fees for pools created by this planner (may be address(0))
constructor(
@@ -59,22 +60,19 @@ contract PartyPlanner is OwnableExternal, IPartyPlanner {
NativeWrapper wrapper_,
PartyPoolSwapImpl swapImpl_,
PartyPoolMintImpl mintImpl_,
IPartyPoolDeployer deployer_,
IPartyPoolDeployer balancedPairDeployer_,
PartyPoolInitCode poolInitCodeStorage_,
PartyPoolBalancedPairInitCode balancedPairInitCodeStorage_,
uint256 protocolFeePpm_,
address protocolFeeAddress_
)
OwnableExternal(owner_)
PartyPoolDeployer(poolInitCodeStorage_, balancedPairInitCodeStorage_)
{
ownableConstructor(owner_);
WRAPPER = wrapper_;
require(address(swapImpl_) != address(0), "Planner: swapImpl address cannot be zero");
SWAP_IMPL = swapImpl_;
require(address(mintImpl_) != address(0), "Planner: mintImpl address cannot be zero");
MINT_IMPL = mintImpl_;
require(address(deployer_) != address(0), "Planner: deployer address cannot be zero");
NORMAL_POOL_DEPLOYER = deployer_;
require(address(balancedPairDeployer_) != address(0), "Planner: balanced pair deployer address cannot be zero");
BALANCED_PAIR_DEPLOYER = balancedPairDeployer_;
require(protocolFeePpm_ < 1_000_000, "Planner: protocol fee >= 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;

View File

@@ -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
);

View File

@@ -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);

View File

@@ -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');
}

View File

@@ -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;
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)
}
}
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_
);
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;
}
}

View File

@@ -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

View File

@@ -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];

View File

@@ -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;
}
}

View File

@@ -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));
}

View File

@@ -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<deposits.length; i++)
deposits[i] = INIT_BAL;
// Perform initial mint
pool.initialMint(address(this), INIT_BAL * tokens.length * 10**18);
token0.mint(address(this), INIT_BAL*2);
token1.mint(address(this), INIT_BAL*2);
token2.mint(address(this), INIT_BAL*2);
token0.approve(address(planner), INIT_BAL*2);
token1.approve(address(planner), INIT_BAL*2);
token2.approve(address(planner), INIT_BAL*2);
vm.prank(planner.owner());
(pool,) = planner.newPool("LP", "LP", tokens, kappa, feePpm, feePpm, false,
address(this), address(this), deposits, 0, 0);
// Deploy pool with zero fees for exact balance matching
poolZeroFee = Deploy.newPartyPool(address(this), "LP_ZERO", "LP_ZERO", tokens, kappa, 0, 0, false);
// Mint additional tokens for zero-fee pool
token0.mint(address(this), INIT_BAL);
token1.mint(address(this), INIT_BAL);
token2.mint(address(this), INIT_BAL);
// Transfer to zero-fee pool
token0.transfer(address(poolZeroFee), INIT_BAL);
token1.transfer(address(poolZeroFee), INIT_BAL);
token2.transfer(address(poolZeroFee), INIT_BAL);
// Initialize zero-fee pool
poolZeroFee.initialMint(address(this), INIT_BAL * tokens.length * 10**18);
vm.prank(planner.owner());
(poolZeroFee,) = planner.newPool("LP_ZERO", "LP_ZERO", tokens, kappa, 0, 0, false,
address(this), address(this), deposits, 0, 0);
// Mint tokens to alice and bob for testing
token0.mint(alice, INIT_BAL);
@@ -153,7 +161,7 @@ contract FundingTest is Test {
token2.mint(bob, INIT_BAL);
// Deploy callback contract
callbackContract = new SwapCallbackContract(address(pool));
callbackContract = new SwapCallbackContract(address(pool), planner);
info = Deploy.newInfo();
}
@@ -184,7 +192,8 @@ contract FundingTest is Test {
maxIn, // maxAmountIn
0, // limitPrice
0, // deadline
false // unwrap
false, // unwrap
''
);
// Verify amounts
@@ -225,7 +234,7 @@ contract FundingTest is Test {
uint256 poolToken1Before = token1.balanceOf(address(poolZeroFee));
// Execute swap
(uint256 amountIn, uint256 amountOut, uint256 fee) = poolZeroFee.swap(
(, uint256 amountOut, uint256 fee) = poolZeroFee.swap(
alice,
Funding.PREFUNDING,
bob,
@@ -234,7 +243,8 @@ contract FundingTest is Test {
maxIn,
0,
0,
false
false,
''
);
// With zero fees, fee should be 0
@@ -270,7 +280,8 @@ contract FundingTest is Test {
maxIn,
0,
0,
false
false,
''
);
vm.stopPrank();
@@ -310,7 +321,8 @@ contract FundingTest is Test {
maxIn, // maxAmountIn
0, // limitPrice
0, // deadline
false // unwrap
false, // unwrap
''
);
// Verify amounts
@@ -335,7 +347,7 @@ contract FundingTest is Test {
uint256 maxIn = 10_000;
// Setup callback for zero-fee pool
SwapCallbackContract zeroFeeCallback = new SwapCallbackContract(address(poolZeroFee));
SwapCallbackContract zeroFeeCallback = new SwapCallbackContract(address(poolZeroFee), planner);
zeroFeeCallback.setTokenSource(alice);
zeroFeeCallback.setShouldFail(false);
@@ -356,7 +368,8 @@ contract FundingTest is Test {
maxIn,
0,
0,
false
false,
''
);
// With zero fees, fee should be 0
@@ -387,7 +400,8 @@ contract FundingTest is Test {
maxIn,
0,
0,
false
false,
''
);
}
@@ -395,28 +409,9 @@ contract FundingTest is Test {
Validation Against swapAmounts()
---------------------- */
/// @notice Verify that pre-funded swap amounts match swapAmounts() view predictions
function testPreFundingMatchesSwapAmountsView() public {
uint256 maxIn = 10_000;
// Perform a reference swap with USE_APPROVALS to get expected amounts
vm.startPrank(bob);
token0.approve(address(pool), type(uint256).max);
(uint256 refAmountIn, uint256 refAmountOut, uint256 refFee) = pool.swap(
bob,
Funding.APPROVALS,
bob,
0,
1,
maxIn,
0,
0,
false
);
vm.stopPrank();
// Reset pool state by creating a fresh pool with identical parameters
function createTestPools2() public returns (IPartyPool testPool1, IPartyPool testPool2) {
// Create two identical test pools
IERC20[] memory tokens = new IERC20[](3);
tokens[0] = IERC20(address(token0));
tokens[1] = IERC20(address(token1));
@@ -424,23 +419,85 @@ contract FundingTest is Test {
uint256 feePpm = 1000;
int128 kappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
PartyPool testPool = Deploy.newPartyPool(address(this), "LP_TEST", "LP_TEST", tokens, kappa, feePpm, feePpm, false);
uint256[] memory deposits = new uint256[](tokens.length);
for(uint256 i=0; i<deposits.length; i++)
deposits[i] = INIT_BAL;
token0.mint(address(this), INIT_BAL);
token1.mint(address(this), INIT_BAL);
token2.mint(address(this), INIT_BAL);
token0.mint(address(this), INIT_BAL*2);
token1.mint(address(this), INIT_BAL*2);
token2.mint(address(this), INIT_BAL*2);
token0.approve(address(planner), INIT_BAL*2);
token1.approve(address(planner), INIT_BAL*2);
token2.approve(address(planner), INIT_BAL*2);
vm.prank(planner.owner());
(testPool1,) = planner.newPool("LP_TEST_1", "LP_TEST_1", tokens, kappa, feePpm, feePpm, false,
address(this), address(this), deposits, INIT_BAL * tokens.length * 10**18, 0);
vm.prank(planner.owner());
(testPool2,) = planner.newPool("LP_TEST_2", "LP_TEST_2", tokens, kappa, feePpm, feePpm, false,
address(this), address(this), deposits, INIT_BAL * tokens.length * 10**18, 0);
}
token0.transfer(address(testPool), INIT_BAL);
token1.transfer(address(testPool), INIT_BAL);
token2.transfer(address(testPool), INIT_BAL);
testPool.initialMint(address(this), INIT_BAL * tokens.length * 10**18);
function createTestPools3() public returns (IPartyPool testPool1, IPartyPool testPool2, IPartyPool testPool3) {
// Create two identical test pools
IERC20[] memory tokens = new IERC20[](3);
tokens[0] = IERC20(address(token0));
tokens[1] = IERC20(address(token1));
tokens[2] = IERC20(address(token2));
// Now test pre-funding with same initial state
uint256 feePpm = 1000;
int128 kappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
uint256[] memory deposits = new uint256[](tokens.length);
for(uint256 i=0; i<deposits.length; i++)
deposits[i] = INIT_BAL;
token0.mint(address(this), INIT_BAL*3);
token1.mint(address(this), INIT_BAL*3);
token2.mint(address(this), INIT_BAL*3);
token0.approve(address(planner), INIT_BAL*3);
token1.approve(address(planner), INIT_BAL*3);
token2.approve(address(planner), INIT_BAL*3);
vm.prank(planner.owner());
(testPool1,) = planner.newPool("LP_TEST_1", "LP_TEST_1", tokens, kappa, feePpm, feePpm, false,
address(this), address(this), deposits, INIT_BAL * tokens.length * 10**18, 0);
vm.prank(planner.owner());
(testPool2,) = planner.newPool("LP_TEST_2", "LP_TEST_2", tokens, kappa, feePpm, feePpm, false,
address(this), address(this), deposits, INIT_BAL * tokens.length * 10**18, 0);
vm.prank(planner.owner());
(testPool3,) = planner.newPool("LP_TEST_3", "LP_TEST_3", tokens, kappa, feePpm, feePpm, false,
address(this), address(this), deposits, INIT_BAL * tokens.length * 10**18, 0);
}
/// @notice Verify that pre-funded swap amounts match swapAmounts() view predictions
function testPreFundingMatchesSwapAmountsView() public {
uint256 maxIn = 10_000;
(IPartyPool testPool1, IPartyPool testPool2) = createTestPools2();
// Perform a reference swap with USE_APPROVALS to get expected amounts
vm.startPrank(bob);
token0.approve(address(testPool1), type(uint256).max);
(uint256 refAmountIn, uint256 refAmountOut, uint256 refFee) = testPool1.swap(
bob,
Funding.APPROVAL,
bob,
0,
1,
maxIn,
0,
0,
false,
''
);
vm.stopPrank();
// Now perform a swap using the prefunding method on the second pool
vm.startPrank(alice);
token0.transfer(address(testPool), maxIn);
token0.transfer(address(testPool2), maxIn);
(uint256 preAmountIn, uint256 preAmountOut, uint256 preFee) = testPool.swap(
(uint256 preAmountIn, uint256 preAmountOut, uint256 preFee) = testPool2.swap(
alice,
Funding.PREFUNDING,
alice,
@@ -449,7 +506,8 @@ contract FundingTest is Test {
maxIn,
0,
0,
false
false,
''
);
vm.stopPrank();
@@ -463,45 +521,28 @@ contract FundingTest is Test {
function testCallbackMatchesSwapAmountsView() public {
uint256 maxIn = 10_000;
(IPartyPool testPool1, IPartyPool testPool2) = createTestPools2();
// Perform a reference swap
vm.startPrank(bob);
token0.approve(address(pool), type(uint256).max);
token0.approve(address(testPool1), type(uint256).max);
(uint256 refAmountIn, uint256 refAmountOut, uint256 refFee) = pool.swap(
(uint256 refAmountIn, uint256 refAmountOut, uint256 refFee) = testPool1.swap(
bob,
Funding.APPROVALS,
Funding.APPROVAL,
bob,
0,
1,
maxIn,
0,
0,
false
false,
''
);
vm.stopPrank();
// Create fresh pool for callback test
IERC20[] memory tokens = new IERC20[](3);
tokens[0] = IERC20(address(token0));
tokens[1] = IERC20(address(token1));
tokens[2] = IERC20(address(token2));
uint256 feePpm = 1000;
int128 kappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
PartyPool testPool = Deploy.newPartyPool(address(this), "LP_TEST2", "LP_TEST2", tokens, kappa, feePpm, feePpm, false);
token0.mint(address(this), INIT_BAL);
token1.mint(address(this), INIT_BAL);
token2.mint(address(this), INIT_BAL);
token0.transfer(address(testPool), INIT_BAL);
token1.transfer(address(testPool), INIT_BAL);
token2.transfer(address(testPool), INIT_BAL);
testPool.initialMint(address(this), INIT_BAL * tokens.length * 10**18);
// Setup callback for test pool
SwapCallbackContract testCallback = new SwapCallbackContract(address(testPool));
SwapCallbackContract testCallback = new SwapCallbackContract(address(testPool2), planner);
testCallback.setTokenSource(alice);
testCallback.setShouldFail(false);
@@ -510,7 +551,7 @@ contract FundingTest is Test {
vm.stopPrank();
// Test callback with same initial state
(uint256 cbAmountIn, uint256 cbAmountOut, uint256 cbFee) = testPool.swap(
(uint256 cbAmountIn, uint256 cbAmountOut, uint256 cbFee) = testPool2.swap(
address(testCallback),
CALLBACK,
alice,
@@ -519,7 +560,8 @@ contract FundingTest is Test {
maxIn,
0,
0,
false
false,
''
);
// Callback amounts should match reference swap amounts
@@ -538,56 +580,26 @@ contract FundingTest is Test {
for (uint i = 0; i < swapAmounts.length; i++) {
uint256 swapAmount = swapAmounts[i];
// Create three identical pools
IERC20[] memory tokens = new IERC20[](3);
tokens[0] = IERC20(address(token0));
tokens[1] = IERC20(address(token1));
tokens[2] = IERC20(address(token2));
(IPartyPool poolApproval, IPartyPool poolPreFund, IPartyPool poolCallback) = createTestPools3();
int128 kappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
PartyPool poolApproval = Deploy.newPartyPool(address(this), "LP_A", "LP_A", tokens, kappa, 0, 0, false);
PartyPool poolPreFund = Deploy.newPartyPool(address(this), "LP_P", "LP_P", tokens, kappa, 0, 0, false);
PartyPool poolCallback = Deploy.newPartyPool(address(this), "LP_C", "LP_C", tokens, kappa, 0, 0, false);
// Initialize all three pools identically
token0.mint(address(this), INIT_BAL * 3);
token1.mint(address(this), INIT_BAL * 3);
token2.mint(address(this), INIT_BAL * 3);
token0.transfer(address(poolApproval), INIT_BAL);
token1.transfer(address(poolApproval), INIT_BAL);
token2.transfer(address(poolApproval), INIT_BAL);
poolApproval.initialMint(address(this), 0);
token0.transfer(address(poolPreFund), INIT_BAL);
token1.transfer(address(poolPreFund), INIT_BAL);
token2.transfer(address(poolPreFund), INIT_BAL);
poolPreFund.initialMint(address(this), 0);
token0.transfer(address(poolCallback), INIT_BAL);
token1.transfer(address(poolCallback), INIT_BAL);
token2.transfer(address(poolCallback), INIT_BAL);
poolCallback.initialMint(address(this), 0);
// Test with USE_APPROVALS (bytes4(0) with approvals)
// Test with APPROVALS
vm.startPrank(alice);
token0.approve(address(poolApproval), type(uint256).max);
(uint256 apprIn, uint256 apprOut, ) = poolApproval.swap(
alice, Funding.APPROVALS, alice, 0, 1, swapAmount, 0, 0, false
alice, Funding.APPROVAL, alice, 0, 1, swapAmount, 0, 0, false, ''
);
vm.stopPrank();
// Test with PRE_FUNDED (bytes4(0) with pre-funding)
// Test with PREFUNDING
vm.startPrank(alice);
token0.transfer(address(poolPreFund), swapAmount);
(uint256 preIn, uint256 preOut, ) = poolPreFund.swap(
alice, Funding.PREFUNDING, alice, 0, 1, swapAmount, 0, 0, false
alice, Funding.PREFUNDING, alice, 0, 1, swapAmount, 0, 0, false, ''
);
vm.stopPrank();
// Test with CALLBACK
SwapCallbackContract cb = new SwapCallbackContract(address(poolCallback));
SwapCallbackContract cb = new SwapCallbackContract(address(poolCallback), planner);
cb.setTokenSource(alice);
cb.setShouldFail(false);
@@ -596,7 +608,7 @@ contract FundingTest is Test {
vm.stopPrank();
(uint256 cbIn, uint256 cbOut, ) = poolCallback.swap(
address(cb), CALLBACK, alice, 0, 1, swapAmount, 0, 0, false
address(cb), CALLBACK, alice, 0, 1, swapAmount, 0, 0, false, ''
);
// All three methods should produce identical results

View File

@@ -13,11 +13,13 @@ import {ERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.s
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {Funding} from "../src/Funding.sol";
import {IPartyPlanner} from "../src/IPartyPlanner.sol";
import {IPartyPool} from "../src/IPartyPool.sol";
import {IPartySwapCallback} from "../src/IPartySwapCallback.sol";
import {LMSRStabilized} from "../src/LMSRStabilized.sol";
import {PartyPlanner} from "../src/PartyPlanner.sol";
import {PartySwapCallbackVerifier} from "../src/PartySwapCallbackVerifier.sol";
import {Deploy} from "./Deploy.sol";
import {TestERC20, FlashBorrower} from "./GasTest.sol";
import {TestERC20, GasHarness, FlashBorrower} from "./GasTest.sol";
/* solhint-disable erc20-unchecked-transfer */
@@ -103,12 +105,58 @@ contract TestERC20 is ERC20 {
}
}
contract GasHarness is IPartySwapCallback {
// In order to compare like-for-like, we need to include the token transfers in a single external function for gas measurement
using SafeERC20 for ERC20;
IPartyPlanner immutable private planner;
constructor(IPartyPlanner planner_) {
planner = planner_;
}
function swapApproval(
IPartyPool pool, IERC20 tokenIn, address /*payer*/, bytes4 fundingSelector, address receiver, uint256 inputTokenIndex,
uint256 outputTokenIndex, uint256 maxAmountIn, int128 limitPrice, uint256 deadline, bool unwrap
) external payable returns (uint256 amountIn, uint256 amountOut, uint256 inFee) {
// pool moves coins
tokenIn.approve(address(pool), type(uint256).max);
(amountIn, amountOut, inFee) = pool.swap{value:msg.value}(address(this), fundingSelector, receiver, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, deadline, unwrap, '');
tokenIn.approve(address(pool), 0);
}
function swapPrefund(
IPartyPool pool, address /*payer*/, bytes4 fundingSelector, address receiver, uint256 inputTokenIndex,
uint256 outputTokenIndex, uint256 maxAmountIn, int128 limitPrice, uint256 deadline, bool unwrap
) external payable returns (uint256 amountIn, uint256 amountOut, uint256 inFee) {
// Prefund the pool
IERC20(pool.token(inputTokenIndex)).transfer(address(pool), maxAmountIn);
return pool.swap{value:msg.value}(address(0), fundingSelector, receiver, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, deadline, unwrap, '');
}
function swapCallback(
IPartyPool pool, address /*payer*/, bytes4 /*fundingSelector*/, address receiver, uint256 inputTokenIndex,
uint256 outputTokenIndex, uint256 maxAmountIn, int128 limitPrice, uint256 deadline, bool unwrap
) external payable returns (uint256 amountIn, uint256 amountOut, uint256 inFee) {
return pool.swap{value:msg.value}(address(this), this.liquidityPartySwapCallback.selector, receiver, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, deadline, unwrap, '');
}
function liquidityPartySwapCallback(bytes32 nonce, IERC20 token, uint256 amount, bytes memory) external {
PartySwapCallbackVerifier.verifyCallback(planner, nonce);
token.transfer(msg.sender, amount);
}
}
/// @notice Gas testing contract for PartyPool - contains all gas measurement tests
contract GasTest is Test {
using ABDKMath64x64 for int128;
using SafeERC20 for TestERC20;
PartyPlanner internal planner;
GasHarness internal harness;
IPartyPlanner internal planner;
IPartyPool internal pool2;
IPartyPool internal pool10;
IPartyPool internal pool20;
@@ -138,6 +186,7 @@ contract GasTest is Test {
// Mint initial balances for pool initialization and test users
token.mint(address(this), INIT_BAL);
token.mint(address(harness), INIT_BAL);
token.mint(alice, INIT_BAL);
token.mint(bob, INIT_BAL);
}
@@ -165,7 +214,7 @@ contract GasTest is Test {
}
/// @notice Helper to create a pool with the stable-pair optimization enabled
function createPoolStable(uint256 numTokens) internal returns (IPartyPool) {
function createPoolStable(uint256 numTokens) internal returns (IPartyPool pool) {
// Deploy _tokens dynamically
address[] memory tokens = new address[](numTokens);
uint256[] memory bases = new uint256[](numTokens);
@@ -178,6 +227,7 @@ contract GasTest is Test {
// Mint initial balances for pool initialization and test users
token.mint(address(this), INIT_BAL);
token.mint(address(harness), INIT_BAL);
token.mint(alice, INIT_BAL);
token.mint(bob, INIT_BAL);
}
@@ -191,17 +241,15 @@ contract GasTest is Test {
ierc20Tokens[i] = IERC20(tokens[i]);
}
int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(ierc20Tokens.length, tradeFrac, targetSlippage);
IPartyPool newPool = Deploy.newPartyPool(address(this), poolName, poolName, ierc20Tokens, computedKappa, feePpm, feePpm, true);
// Transfer initial deposit amounts into pool before initial mint
uint256[] memory initialBalances = new uint256[](numTokens);
for (uint256 i = 0; i < numTokens; i++) {
TestERC20(tokens[i]).transfer(address(newPool), INIT_BAL);
initialBalances[i] = INIT_BAL;
ierc20Tokens[i].approve(address(planner), INIT_BAL);
}
// Perform initial mint (initial deposit); receiver is this contract
newPool.initialMint(address(this), 0);
return newPool;
vm.prank(planner.owner());
(pool, ) = planner.newPool(poolName, poolName, ierc20Tokens, computedKappa, feePpm, feePpm, true,
address(this), address(this), initialBalances, 0, 0);
}
function setUp() public {
@@ -210,6 +258,8 @@ contract GasTest is Test {
planner = Deploy.newPartyPlanner();
harness = new GasHarness(planner);
// Configure LMSR parameters similar to other tests: trade size 1% of asset -> 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);

View File

@@ -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();

View File

@@ -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;

View File

@@ -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");