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

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

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

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

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