per-asset fees

This commit is contained in:
tim
2025-10-29 18:22:23 -04:00
parent 86410c9a91
commit 20758cfb35
18 changed files with 475 additions and 164 deletions

View File

@@ -61,8 +61,8 @@ interface IPartyPool is IERC20Metadata, IOwnable {
IERC20 indexed tokenOut,
uint256 amountIn,
uint256 amountOut,
uint256 lpFee,
uint256 protocolFee
uint256 lpFee, // taken from the output token
uint256 protocolFee // taken from the output token
);
event Flash(
@@ -97,8 +97,11 @@ interface IPartyPool is IERC20Metadata, IOwnable {
/// @dev denominators()[i] is the base for tokens[i]. These bases are chosen by deployer and must match token decimals.
function denominators() external view returns (uint256[] memory);
/// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations.
function swapFeePpm() external view returns (uint256);
/// @notice Per-asset swap fees in ppm. Fees are applied on input; for asset-to-asset swaps, the effective pair fee is 1 - (1 - f_i)(1 - f_j).
function fees() external view returns (uint256[] memory);
/// @notice Effective combined fee in ppm for the given asset pair (i as input, j as output).
function fee(uint256 i, uint256 j) external view returns (uint256);
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
function flashFeePpm() external view returns (uint256);
@@ -163,13 +166,13 @@ interface IPartyPool is IERC20Metadata, IOwnable {
/// @param outputTokenIndex index of output token
/// @param maxAmountIn maximum gross input allowed (inclusive of fee)
/// @param limitPrice maximum acceptable marginal price (pass 0 to ignore)
/// @return amountIn gross input amount to transfer (includes fee), amountOut output amount user would receive, fee fee amount taken
/// @return amountIn gross input amount to transfer (includes fee), amountOut output amount user would receive, inFee fee taken from input amount
function swapAmounts(
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee);
) external view returns (uint256 amountIn, uint256 amountOut, uint256 inFee);
/// @notice Swap input token inputTokenIndex -> token outputTokenIndex. Payer must approve token inputTokenIndex.
/// @dev This function transfers the exact gross input (including fee) from payer and sends the computed output to receiver.
@@ -181,7 +184,7 @@ 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.
/// @return amountIn actual input used (uint256), amountOut actual output sent (uint256), fee fee taken from the input (uint256)
/// @return amountIn actual input used (uint256), amountOut actual output sent (uint256), inFee fee taken from the input (uint256)
function swap(
address payer,
address receiver,
@@ -191,7 +194,7 @@ interface IPartyPool is IERC20Metadata, IOwnable {
int128 limitPrice,
uint256 deadline,
bool unwrap
) external payable returns (uint256 amountIn, uint256 amountOut, uint256 fee);
) 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.
@@ -202,7 +205,7 @@ interface IPartyPool is IERC20Metadata, IOwnable {
/// @param outputTokenIndex index of output asset
/// @param limitPrice target marginal price to reach (must be > 0)
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
/// @return amountInUsed actual input used excluding fee (uint256), amountOut actual output sent (uint256), fee fee taken from the input (uint256)
/// @return amountInUsed actual input used excluding fee (uint256), amountOut actual output sent (uint256), inFee fee taken from the input (uint256)
function swapToLimit(
address payer,
address receiver,
@@ -211,7 +214,7 @@ interface IPartyPool is IERC20Metadata, IOwnable {
int128 limitPrice,
uint256 deadline,
bool unwrap
) external payable returns (uint256 amountInUsed, uint256 amountOut, uint256 fee);
) external payable returns (uint256 amountInUsed, uint256 amountOut, uint256 inFee);
/// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP.
/// @dev swapMint executes as an exact-in planned swap followed by proportional scaling of qInternal.
@@ -221,14 +224,14 @@ interface IPartyPool is IERC20Metadata, IOwnable {
/// @param inputTokenIndex index of the input token
/// @param maxAmountIn maximum uint token input (inclusive of fee)
/// @param deadline optional deadline
/// @return lpMinted actual LP minted (uint)
/// @return amountInUsed actual input used (uint256), lpMinted actual LP minted (uint256), inFee fee taken from the input (uint256)
function swapMint(
address payer,
address receiver,
uint256 inputTokenIndex,
uint256 maxAmountIn,
uint256 deadline
) external payable returns (uint256 lpMinted);
) external payable returns (uint256 amountInUsed, uint256 lpMinted, uint256 inFee);
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `outputTokenIndex` and send to receiver.
/// @dev The function burns LP tokens (authorization via allowance if needed), sends the single-asset payout and updates LMSR state.

View File

@@ -34,27 +34,27 @@ interface IPartyPoolViewer {
/// @param inputTokenIndex index of input token
/// @param outputTokenIndex index of output token
/// @param limitPrice target marginal price to reach (must be > 0)
/// @return amountIn gross input amount to transfer (includes fee), amountOut output amount user would receive, fee fee amount taken
/// @return amountIn gross input amount to transfer (includes fee), amountOut output amount user would receive, inFee fee taken from input amount
function swapToLimitAmounts(
IPartyPool pool,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
int128 limitPrice
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee);
) external view returns (uint256 amountIn, uint256 amountOut, uint256 inFee);
/// @notice Calculate the amounts for a swap mint operation
/// @dev This is a pure view function that computes swap mint amounts from provided state
/// @param inputTokenIndex index of the input token
/// @param maxAmountIn maximum amount of token to deposit (inclusive of fee)
function swapMintAmounts(IPartyPool pool, uint256 inputTokenIndex, uint256 maxAmountIn) external view
returns (uint256 amountInUsed, uint256 fee, uint256 lpMinted);
returns (uint256 amountInUsed, uint256 lpMinted, uint256 inFee);
/// @notice Calculate the amounts for a burn swap operation
/// @dev This is a pure view function that computes burn swap amounts from provided state
/// @param lpAmount amount of LP _tokens to burn
/// @param outputTokenIndex index of target asset to receive
function burnSwapAmounts(IPartyPool pool, uint256 lpAmount, uint256 outputTokenIndex) external view
returns (uint256 amountOut);
returns (uint256 amountOut, uint256 outFee);
/// @notice Compute repayment amounts (principal + flash fee) for a proposed flash loan.
/// @param loanAmounts array of per-token loan amounts; must match the pool's token ordering.

View File

@@ -131,8 +131,6 @@ library LMSRStabilized {
int128 a,
int128 limitPrice
) internal pure returns (int128 amountIn, int128 amountOut) {
require(i < nAssets && j < nAssets, "LMSR: idx");
// Initialize amountIn to full amount (will be adjusted if limit price is hit)
amountIn = a;
@@ -140,48 +138,37 @@ library LMSRStabilized {
int128 sizeMetric = _computeSizeMetric(qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
require(b > int128(0), "LMSR: b<=0");
// Precompute reciprocal of b to avoid repeated divisions
int128 invB = ABDKMath64x64.div(ONE, b);
// Guard: output asset must have non-zero effective weight to avoid degenerate/div-by-zero-like conditions
require(qInternal[j] > int128(0), "LMSR: e_j==0");
// Compute r0 = exp((q_i - q_j) / b) directly using invB
int128 r0 = _exp(qInternal[i].sub(qInternal[j]).mul(invB));
require(r0 > int128(0), "LMSR: r0<=0"); // equivalent to e_j > 0 check
// If a positive limitPrice is given, determine whether the full `a` would
// push the marginal price p_i/p_j beyond the limit; if so, truncate `a`.
// Marginal price ratio evolves as r(t) = r0 * exp(t/b) (since e_i multiplies by exp(t/b))
if (limitPrice > int128(0)) {
// r0 must be positive; if r0 == 0 then no risk of exceeding limit by increasing r.
require(r0 >= int128(0), "LMSR: r0<0");
if (r0 == int128(0)) {
// console2.log("r0 == 0 (input asset has zero weight), no limit truncation needed");
// If limitPrice <= current price, we revert (caller must choose a limit > current price to allow any fill)
if (limitPrice <= r0) {
revert("LMSR: limitPrice <= current price");
}
// Compute a_limit directly from ln(limit / r0): a_limit = b * ln(limit / r0)
int128 ratioLimitOverR0 = limitPrice.div(r0);
require(ratioLimitOverR0 > int128(0), "LMSR: ratio<=0");
int128 aLimitOverB = _ln(ratioLimitOverR0); // > 0
// aLimit = b * aLimitOverB
int128 aLimit64 = b.mul(aLimitOverB);
// If computed aLimit is less than the requested a, use the truncated value.
if (aLimit64 < a) {
amountIn = aLimit64; // Store the truncated input amount
a = aLimit64; // Use truncated amount for calculations
} else {
// If limitPrice <= current price, we revert (caller must choose a limit > current price to allow any fill)
if (limitPrice <= r0) {
revert("LMSR: limitPrice <= current price");
}
// Compute a_limit directly from ln(limit / r0): a_limit = b * ln(limit / r0)
int128 ratioLimitOverR0 = limitPrice.div(r0);
require(ratioLimitOverR0 > int128(0), "LMSR: ratio<=0");
int128 aLimitOverB = _ln(ratioLimitOverR0); // > 0
// aLimit = b * aLimitOverB
int128 aLimit64 = b.mul(aLimitOverB);
// If computed aLimit is less than the requested a, use the truncated value.
if (aLimit64 < a) {
amountIn = aLimit64; // Store the truncated input amount
a = aLimit64; // Use truncated amount for calculations
} else {
// console2.log("Not truncating: aLimit64 >= a");
}
// no truncation needed
}
}
@@ -255,28 +242,19 @@ library LMSRStabilized {
uint256 j,
int128 limitPrice
) internal pure returns (int128 amountIn, int128 amountOut) {
require(i < nAssets && j < nAssets, "LMSR: idx");
require(limitPrice > int128(0), "LMSR: limitPrice <= 0");
// Compute b and ensure positivity before deriving invB
int128 sizeMetric = _computeSizeMetric(qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
require(b > int128(0), "LMSR: b<=0");
// Precompute reciprocal of b to avoid repeated divisions
int128 invB = ABDKMath64x64.div(ONE, b);
// Guard: output asset must have non-zero effective weight to avoid degenerate/div-by-zero-like conditions
require(qInternal[j] > int128(0), "LMSR: e_j==0");
// Compute r0 = exp((q_i - q_j) / b) directly using invB
int128 r0 = _exp(qInternal[i].sub(qInternal[j]).mul(invB));
// Mirror swapAmountsForExactInput behavior: treat invalid r0 as an error condition.
// Revert if r0 is non-positive (no finite trade under a price limit).
require(r0 > int128(0), "LMSR: r0<=0");
// If current price already exceeds or equals limit, revert the same way swapAmountsForExactInput does.
if (r0 >= limitPrice) {
revert("LMSR: limitPrice <= current price");

View File

@@ -81,14 +81,14 @@ contract PartyPlanner is OwnableExternal, IPartyPlanner {
protocolFeeAddress = protocolFeeAddress_;
}
/// Main newPool variant: accepts kappa directly (preferred).
/// Main newPool variant: accepts kappa directly (preferred) and a per-asset fee vector.
function newPool(
// Pool constructor args
string memory name_,
string memory symbol_,
IERC20[] memory tokens_,
int128 kappa_,
uint256 swapFeePpm_,
uint256[] memory swapFeesPpm_,
uint256 flashFeePpm_,
bool stable_,
// Initial deposit information
@@ -107,6 +107,9 @@ contract PartyPlanner is OwnableExternal, IPartyPlanner {
// Validate kappa > 0 (Q64.64)
require(kappa_ > int128(0), "Planner: kappa must be > 0");
// Validate fees vector length matches number of tokens
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(
@@ -115,7 +118,7 @@ contract PartyPlanner is OwnableExternal, IPartyPlanner {
symbol_,
tokens_,
kappa_,
swapFeePpm_,
swapFeesPpm_,
flashFeePpm_,
PROTOCOL_FEE_PPM,
protocolFeeAddress,
@@ -155,6 +158,48 @@ contract PartyPlanner is OwnableExternal, IPartyPlanner {
lpAmount = pool.initialMint(receiver, initialLpAmount);
}
/// Convenience overload: legacy single-fee signature — repeat the scalar for every asset and delegate.
function newPool(
// Pool constructor args (legacy single-fee)
string memory name_,
string memory symbol_,
IERC20[] memory tokens_,
int128 kappa_,
uint256 swapFeePpm_,
uint256 flashFeePpm_,
bool stable_,
// Initial deposit information
address payer,
address receiver,
uint256[] memory initialDeposits,
uint256 initialLpAmount,
uint256 deadline
) public onlyOwner returns (IPartyPool pool, uint256 lpAmount) {
// Build per-asset fee vector by repeating the scalar swapFeePpm_
uint256[] memory feesArr = new uint256[](tokens_.length);
for (uint256 i = 0; i < tokens_.length; i++) {
// We divide by two, because the new per-asset fee semantics charges both the in-asset fee and
// out-asset fee. This should be a square-root for exactness.
feesArr[i] = swapFeePpm_ / 2;
}
// Delegate to the vector-based newPool variant
return newPool(
name_,
symbol_,
tokens_,
kappa_,
feesArr,
flashFeePpm_,
stable_,
payer,
receiver,
initialDeposits,
initialLpAmount,
deadline
);
}
// NOTE that the slippage target is only exactly achieved in completely balanced pools where all assets are
// priced the same. This target is actually a minimum slippage that the pool imposes on traders, and the actual
// slippage cost can be multiples bigger in practice due to pool inventory imbalances.

View File

@@ -54,9 +54,11 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
int128 private immutable KAPPA; // kappa in Q64.64
function kappa() external view returns (int128) { return KAPPA; }
/// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations.
uint256 private immutable SWAP_FEE_PPM;
function swapFeePpm() external view returns (uint256) { return SWAP_FEE_PPM; }
/// @notice Per-asset swap fees in ppm.
function fees() external view returns (uint256[] memory) { return _fees; }
/// @notice Effective combined fee in ppm for (i as input, j as output)
function fee(uint256 i, uint256 j) external view returns (uint256) { return _pairFeePpm(i,j); }
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
uint256 private immutable FLASH_FEE_PPM;
@@ -100,7 +102,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
/// @param symbol_ LP token symbol
/// @param tokens_ token addresses (n)
/// @param kappa_ liquidity parameter κ (Q64.64) used to derive b = κ * S(q)
/// @param swapFeePpm_ fee in parts-per-million, taken from swap input amounts before LMSR calculations
/// @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
@@ -110,7 +112,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
string memory symbol_,
IERC20[] memory tokens_,
int128 kappa_,
uint256 swapFeePpm_,
uint256[] memory fees_,
uint256 flashFeePpm_,
uint256 protocolFeePpm_,
address protocolFeeAddress_,
@@ -126,11 +128,17 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
require(tokens_.length > 1, "Pool: need >1 asset");
_tokens = tokens_;
KAPPA = kappa_;
require(swapFeePpm_ < 1_000_000, "Pool: fee >= ppm");
SWAP_FEE_PPM = swapFeePpm_;
require(flashFeePpm_ < 1_000_000, "Pool: flash fee >= ppm");
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_ < 1_000_000, "Pool: protocol fee >= ppm");
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_;
@@ -168,8 +176,10 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
/// @notice If a security problem is found, the vault owner may call this function to permanently disable swap and
/// mint functionality, leaving only burns (withdrawals) working.
function kill() external onlyOwner {
_killed = true;
emit Killed();
if( !_killed ) {
_killed = true;
emit Killed();
}
}
/* ----------------------
@@ -232,7 +242,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
) external view returns (uint256 amountIn, uint256 amountOut, uint256 inFee) {
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice);
return (grossIn, outUint, feeUint);
}
@@ -247,7 +257,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
int128 limitPrice,
uint256 deadline,
bool unwrap
) external payable native nonReentrant killable returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
) external payable native nonReentrant killable returns (uint256 amountIn, uint256 amountOut, uint256 inFee) {
require(deadline == 0 || block.timestamp <= deadline, "swap: deadline exceeded");
// Compute amounts using the same path as views
@@ -303,10 +313,8 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice
)
internal
view
returns (
) internal view
returns (
uint256 grossIn,
uint256 amountOutUint,
int128 amountInInternalUsed,
@@ -316,7 +324,8 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
)
{
// Estimate max net input (fee on gross rounded up, then subtract)
(, uint256 netUintForSwap) = _computeFee(maxAmountIn, SWAP_FEE_PPM);
uint256 pairFeePpm = _pairFeePpm(inputTokenIndex, outputTokenIndex);
(, uint256 netUintForSwap) = _computeFee(maxAmountIn, pairFeePpm);
// Convert to internal (floor)
int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, _bases[inputTokenIndex]);
@@ -332,8 +341,8 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
// Compute gross transfer including fee on the used input (ceil)
feeUint = 0;
grossIn = amountInUintNoFee;
if (SWAP_FEE_PPM > 0) {
feeUint = _ceilFee(amountInUintNoFee, SWAP_FEE_PPM);
if (pairFeePpm > 0) {
feeUint = _ceilFee(amountInUintNoFee, pairFeePpm);
grossIn += feeUint;
}
@@ -354,7 +363,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
int128 limitPrice,
uint256 deadline,
bool unwrap
) external payable returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) {
) external payable returns (uint256 amountInUsed, uint256 amountOut, uint256 inFee) {
bytes memory data = abi.encodeWithSelector(
PartyPoolSwapImpl.swapToLimit.selector,
payer,
@@ -364,7 +373,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
limitPrice,
deadline,
unwrap,
SWAP_FEE_PPM,
_pairFeePpm(inputTokenIndex, outputTokenIndex),
PROTOCOL_FEE_PPM
);
bytes memory result = Address.functionDelegateCall(address(SWAP_IMPL), data);
@@ -379,14 +388,14 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
/// @param inputTokenIndex index of the input token
/// @param maxAmountIn maximum uint token input (inclusive of fee)
/// @param deadline optional deadline
/// @return lpMinted actual LP minted (uint)
/// @return amountInUsed actual input used (uint256), lpMinted actual LP minted (uint256), inFee fee taken from the input (uint256)
function swapMint(
address payer,
address receiver,
uint256 inputTokenIndex,
uint256 maxAmountIn,
uint256 deadline
) external payable returns (uint256 lpMinted) {
) external payable returns (uint256 amountInUsed, uint256 lpMinted, uint256 inFee) {
bytes memory data = abi.encodeWithSelector(
PartyPoolMintImpl.swapMint.selector,
payer,
@@ -394,12 +403,12 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
inputTokenIndex,
maxAmountIn,
deadline,
SWAP_FEE_PPM,
_assetFeePpm(inputTokenIndex),
PROTOCOL_FEE_PPM
);
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
return abi.decode(result, (uint256));
return abi.decode(result, (uint256, uint256, uint256));
}
/// @notice Burn LP _tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver.
@@ -426,7 +435,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
outputTokenIndex,
deadline,
unwrap,
SWAP_FEE_PPM,
_assetFeePpm(outputTokenIndex),
PROTOCOL_FEE_PPM
);
@@ -435,13 +444,11 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
}
bytes32 internal constant FLASH_CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
/**
* @dev Loan `amount` _tokens to `receiver`, and takes it back plus a `flashFee` after the callback.
* @param receiver The contract receiving the _tokens, needs to implement the `onFlashLoan(address user, uint256 amount, uint256 fee, bytes calldata)` interface.
* @dev Loan `amount` tokens to `receiver`, and takes it back plus a `flashFee` after the callback.
* @param receiver The contract receiving the tokens, needs to implement the `onFlashLoan(address user, uint256 amount, uint256 fee, bytes calldata)` interface.
* @param tokenAddr The loan currency.
* @param amount The amount of _tokens lent.
* @param amount The amount of tokens lent.
* @param data A data parameter to be passed on to the `receiver` for any custom use.
*/
function flashLoan(
@@ -449,37 +456,19 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
address tokenAddr,
uint256 amount,
bytes calldata data
) external nonReentrant killable returns (bool)
) external returns (bool)
{
IERC20 token = IERC20(tokenAddr);
require(amount <= token.balanceOf(address(this)));
uint256 tokenIndex = _tokenAddressToIndexPlusOne[token];
require(tokenIndex != 0, 'flash: token not in pool');
tokenIndex -= 1;
(uint256 fee, ) = _computeFee(amount, FLASH_FEE_PPM);
// Compute protocol share of flash fee
uint256 protoShare = 0;
if (PROTOCOL_FEE_PPM > 0 && fee > 0) {
protoShare = (fee * PROTOCOL_FEE_PPM) / 1_000_000; // floor
if (protoShare > 0) {
_protocolFeesOwed[tokenIndex] += protoShare;
}
}
_sendTokenTo(token, address(receiver), amount, false);
require(receiver.onFlashLoan(msg.sender, address(token), amount, fee, data) == FLASH_CALLBACK_SUCCESS);
_receiveTokenFrom(address(receiver), token, amount + fee);
// Update cached balance for the borrowed token
uint256 balAfter = token.balanceOf(address(this));
// Inline _recordCachedBalance logic
require(balAfter >= _protocolFeesOwed[tokenIndex], "balance < protocol owed");
_cachedUintBalances[tokenIndex] = balAfter - _protocolFeesOwed[tokenIndex];
emit Flash(msg.sender, receiver, token, amount, fee-protoShare, protoShare);
return true;
bytes memory payload = abi.encodeWithSelector(
PartyPoolSwapImpl.flashLoan.selector,
receiver,
tokenAddr,
amount,
data,
FLASH_FEE_PPM,
PROTOCOL_FEE_PPM
);
bytes memory result = Address.functionDelegateCall(address(SWAP_IMPL), payload);
return abi.decode(result, (bool));
}
@@ -490,7 +479,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
PartyPoolSwapImpl.collectProtocolFees.selector,
protocolFeeAddress
);
Address.functionDelegateCall(address(MINT_IMPL), data);
Address.functionDelegateCall(address(SWAP_IMPL), data);
}

View File

@@ -16,7 +16,7 @@ contract PartyPoolBalancedPair is PartyPool {
string memory symbol_,
IERC20[] memory tokens_,
int128 kappa_,
uint256 swapFeePpm_,
uint256[] memory fees_,
uint256 flashFeePpm_,
uint256 protocolFeePpm_, // NEW: protocol share of fees (ppm)
address protocolFeeAddress_, // NEW: recipient for collected protocol tokens
@@ -24,7 +24,7 @@ contract PartyPoolBalancedPair is PartyPool {
PartyPoolSwapImpl swapMintImpl_,
PartyPoolMintImpl mintImpl_
)
PartyPool(owner_, name_, symbol_, tokens_, kappa_, swapFeePpm_, flashFeePpm_, protocolFeePpm_, protocolFeeAddress_, wrapperToken_, swapMintImpl_, 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

View File

@@ -24,6 +24,9 @@ abstract contract PartyPoolBase is OwnableInternal, ERC20Internal, ReentrancyGua
WRAPPER_TOKEN = wrapper_;
}
/// @notice Per-asset swap fees in ppm. Fees are applied on input for swaps; see helpers for composition rules.
uint256[] internal _fees;
//
// Internal state
//
@@ -78,6 +81,23 @@ abstract contract PartyPoolBase is OwnableInternal, ERC20Internal, ReentrancyGua
Conversion & fee helpers (internal)
---------------------- */
// Per-asset fee getters and composition
function _assetFeePpm(uint256 i) internal view returns (uint256) {
if (_fees.length == 0) return 0;
return _fees[i];
}
// Effective pair fee: 1 - (1-fi)(1-fj) in ppm, rounding in favor of the pool, and guarding
// overflows by using 1e6 ppm base.
// We implement this as: ceil( fi + fj - (fi*fj)/1e6 ) for the real-valued expression.
// For integer arithmetic with fi,fj in ppm this is equal to: fi + fj - floor( (fi*fj)/1e6 ).
// So we compute prod = fi * fj, prodDiv = prod / 1e6 (floor), and return fi + fj - prodDiv.
function _pairFeePpm(uint256 i, uint256 j) internal view returns (uint256) {
uint256 fi = _fees[i];
uint256 fj = _fees[j];
return fi + fj - fi * fj / 1_000_000;
}
// Convert uint token amount -> internal 64.64 (floor). Uses ABDKMath64x64.divu which truncates.
function _uintToInternalFloor(uint256 amount, uint256 base) internal pure returns (int128) {
// internal = amount / base (as Q64.64)

View File

@@ -16,7 +16,7 @@ interface IPartyPoolDeployer {
string memory symbol_,
IERC20[] memory tokens_,
int128 kappa_,
uint256 swapFeePpm_,
uint256[] memory fees_,
uint256 flashFeePpm_,
uint256 protocolFeePpm_,
address protocolFeeAddress_,
@@ -33,7 +33,7 @@ contract PartyPoolDeployer is IPartyPoolDeployer {
string memory symbol_,
IERC20[] memory tokens_,
int128 kappa_,
uint256 swapFeePpm_,
uint256[] memory fees_,
uint256 flashFeePpm_,
uint256 protocolFeePpm_,
address protocolFeeAddress_,
@@ -47,7 +47,7 @@ contract PartyPoolDeployer is IPartyPoolDeployer {
symbol_,
tokens_,
kappa_,
swapFeePpm_,
fees_,
flashFeePpm_,
protocolFeePpm_,
protocolFeeAddress_,
@@ -65,7 +65,7 @@ contract PartyPoolBalancedPairDeployer is IPartyPoolDeployer {
string memory symbol_,
IERC20[] memory tokens_,
int128 kappa_,
uint256 swapFeePpm_,
uint256[] memory fees_,
uint256 flashFeePpm_,
uint256 protocolFeePpm_,
address protocolFeeAddress_,
@@ -79,7 +79,7 @@ contract PartyPoolBalancedPairDeployer is IPartyPoolDeployer {
symbol_,
tokens_,
kappa_,
swapFeePpm_,
fees_,
flashFeePpm_,
protocolFeePpm_,
protocolFeeAddress_,

View File

@@ -268,8 +268,8 @@ contract PartyPoolMintImpl is PartyPoolBase {
/// @param bases_ scaling _bases for each token
/// @param totalSupply_ current total LP token supply
/// @return amountInUsed actual input amount used (excluding fee)
/// @return fee fee amount charged
/// @return lpMinted LP _tokens that would be minted
/// @return lpMinted LP tokens that would be minted
/// @return inFee fee amount charged
function swapMintAmounts(
uint256 inputTokenIndex,
uint256 maxAmountIn,
@@ -277,7 +277,7 @@ contract PartyPoolMintImpl is PartyPoolBase {
LMSRStabilized.State memory lmsrState,
uint256[] memory bases_,
uint256 totalSupply_
) public pure returns (uint256 amountInUsed, uint256 fee, uint256 lpMinted) {
) public pure returns (uint256 amountInUsed, uint256 lpMinted, uint256 inFee) {
require(inputTokenIndex < bases_.length, "swapMintAmounts: idx");
require(maxAmountIn > 0, "swapMintAmounts: input zero");
require(lmsrState.nAssets > 0, "swapMintAmounts: uninit pool");
@@ -304,11 +304,11 @@ contract PartyPoolMintImpl is PartyPoolBase {
require(amountInUsed > 0, "swapMintAmounts: input zero after internal conversion");
// Compute fee on the actual used input (ceiling)
fee = 0;
inFee = 0;
if (swapFeePpm > 0) {
fee = (amountInUsed * swapFeePpm + 999999) / 1000000; // ceil fee
inFee = (amountInUsed * swapFeePpm + 999999) / 1000000; // ceil fee
}
uint256 totalTransfer = amountInUsed + fee;
uint256 totalTransfer = amountInUsed + inFee;
require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMintAmounts: transfer exceeds max");
// Compute old and new scaled size metrics to determine LP minted
@@ -345,7 +345,7 @@ contract PartyPoolMintImpl is PartyPoolBase {
/// @param maxAmountIn maximum uint token input (inclusive of fee)
/// @param deadline optional deadline
/// @param swapFeePpm fee in parts-per-million for this pool
/// @return lpMinted actual LP minted (uint)
/// @return amountInUsed actual input used (uint256), lpMinted actual LP minted (uint256), inFee fee taken from the input (uint256)
function swapMint(
address payer,
address receiver,
@@ -354,7 +354,7 @@ contract PartyPoolMintImpl is PartyPoolBase {
uint256 deadline,
uint256 swapFeePpm,
uint256 protocolFeePpm
) external payable native killable nonReentrant returns (uint256 lpMinted) {
) external payable native killable nonReentrant returns (uint256 amountInUsed, uint256 lpMinted, uint256 inFee) {
uint256 n = _tokens.length;
require(inputTokenIndex < n, "swapMint: idx");
require(maxAmountIn > 0, "swapMint: input zero");
@@ -435,7 +435,10 @@ contract PartyPoolMintImpl is PartyPoolBase {
emit IPartyPool.SwapMint(payer, receiver, _tokens[inputTokenIndex],
totalTransfer, actualLpToMint, feeUintActual-protoShare, protoShare);
return actualLpToMint;
amountInUsed = amountInUint;
lpMinted = actualLpToMint;
inFee = feeUintActual;
return (amountInUsed, lpMinted, inFee);
}
/// @notice Calculate the amounts for a burn swap operation
@@ -454,7 +457,7 @@ contract PartyPoolMintImpl is PartyPoolBase {
LMSRStabilized.State memory lmsrState,
uint256[] memory bases_,
uint256 totalSupply_
) public pure returns (uint256 amountOut) {
) public pure returns (uint256 amountOut, uint256 outFee) {
require(outputTokenIndex < bases_.length, "burnSwapAmounts: idx");
require(lpAmount > 0, "burnSwapAmounts: zero lp");
require(totalSupply_ > 0, "burnSwapAmounts: empty supply");
@@ -470,6 +473,13 @@ contract PartyPoolMintImpl is PartyPoolBase {
// Convert payoutInternal -> uint (floor) to favor pool
amountOut = _internalToUintFloorPure(payoutInternal, bases_[outputTokenIndex]);
require(amountOut > 0, "burnSwapAmounts: output zero");
// Compute gross payout (no swap fee) to derive token-side fee = gross - net
int128 alphaGross = ABDKMath64x64.divu(lpAmount, totalSupply_); // gross fraction (no swap fee)
(int128 payoutGrossInternal, ) = LMSRStabilized.swapAmountsForBurn(lmsrState.nAssets, lmsrState.kappa, lmsrState.qInternal,
outputTokenIndex, alphaGross);
uint256 payoutGrossUint = _internalToUintFloorPure(payoutGrossInternal, bases_[outputTokenIndex]);
outFee = (payoutGrossUint > amountOut) ? (payoutGrossUint - amountOut) : 0;
}
/// @notice Burn LP _tokens then swap the redeemed proportional basket into a single asset `outputTokenIndex` and send to receiver.

View File

@@ -8,6 +8,7 @@ import {IPartyPool} from "./IPartyPool.sol";
import {NativeWrapper} from "./NativeWrapper.sol";
import {LMSRStabilized} from "./LMSRStabilized.sol";
import {PartyPoolBase} from "./PartyPoolBase.sol";
import {IERC3156FlashBorrower} from "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sol";
/// @title PartyPoolSwapMintImpl - Implementation contract for swapMint and burnSwap functions
/// @notice This contract contains the swapMint and burnSwap implementation that will be called via delegatecall
@@ -19,6 +20,48 @@ contract PartyPoolSwapImpl is PartyPoolBase {
constructor(NativeWrapper wrapper_) PartyPoolBase(wrapper_) {}
bytes32 internal constant FLASH_CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
function flashLoan(
IERC3156FlashBorrower receiver,
address tokenAddr,
uint256 amount,
bytes calldata data,
uint256 flashFeePpm,
uint256 protocolFeePpm
) external nonReentrant killable returns (bool) {
IERC20 token = IERC20(tokenAddr);
require(amount <= token.balanceOf(address(this)), "flash: amount > balance");
uint256 tokenIndex = _tokenAddressToIndexPlusOne[token];
require(tokenIndex != 0, 'flash: token not in pool');
tokenIndex -= 1;
(uint256 flashFee, ) = _computeFee(amount, flashFeePpm);
// Compute protocol share of flash fee
uint256 protoShare = 0;
if (protocolFeePpm > 0 && flashFee > 0) {
protoShare = (flashFee * protocolFeePpm) / 1_000_000; // floor
if (protoShare > 0) {
_protocolFeesOwed[tokenIndex] += protoShare;
}
}
_sendTokenTo(token, address(receiver), amount, false);
require(
receiver.onFlashLoan(msg.sender, address(token), amount, flashFee, data) == FLASH_CALLBACK_SUCCESS,
'flash: callback'
);
_receiveTokenFrom(address(receiver), token, amount + flashFee);
// Update cached balance for the borrowed token
uint256 balAfter = token.balanceOf(address(this));
require(balAfter >= _protocolFeesOwed[tokenIndex], "balance < protocol owed");
_cachedUintBalances[tokenIndex] = balAfter - _protocolFeesOwed[tokenIndex];
emit IPartyPool.Flash(msg.sender, receiver, token, amount, flashFee - protoShare, protoShare);
return true;
}
function swapToLimitAmounts(
uint256 inputTokenIndex,
uint256 outputTokenIndex,
@@ -27,7 +70,7 @@ contract PartyPoolSwapImpl is PartyPoolBase {
int128 kappa,
int128[] memory qInternal,
uint256 swapFeePpm
) external pure returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
) external pure returns (uint256 amountIn, uint256 amountOut, uint256 inFee) {
// Compute internal maxima at the price limit
(int128 amountInInternal, int128 amountOutInternal) = LMSRStabilized.swapAmountsForPriceLimit(
bases.length, kappa, qInternal,
@@ -37,11 +80,11 @@ contract PartyPoolSwapImpl is PartyPoolBase {
uint256 amountInUintNoFee = _internalToUintCeil(amountInInternal, bases[inputTokenIndex]);
require(amountInUintNoFee > 0, "swapToLimit: input zero");
fee = 0;
inFee = 0;
amountIn = amountInUintNoFee;
if (swapFeePpm > 0) {
fee = _ceilFee(amountInUintNoFee, swapFeePpm);
amountIn += fee;
inFee = _ceilFee(amountInUintNoFee, swapFeePpm);
amountIn += inFee;
}
amountOut = _internalToUintFloor(amountOutInternal, bases[outputTokenIndex]);
@@ -59,7 +102,7 @@ contract PartyPoolSwapImpl is PartyPoolBase {
bool unwrap,
uint256 swapFeePpm,
uint256 protocolFeePpm
) external payable native killable nonReentrant returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) {
) external payable native killable nonReentrant returns (uint256 amountInUsed, uint256 amountOut, uint256 inFee) {
uint256 n = _tokens.length;
require(inputTokenIndex < n && outputTokenIndex < n, "swapToLimit: idx");
require(limitPrice > int128(0), "swapToLimit: limit <= 0");

View File

@@ -91,13 +91,13 @@ contract PartyPoolViewer is PartyPoolHelpers, IPartyPoolViewer {
/// @param inputTokenIndex index of input token
/// @param outputTokenIndex index of output token
/// @param limitPrice target marginal price to reach (must be > 0)
/// @return amountIn gross input amount to transfer (includes fee), amountOut output amount user would receive, fee fee amount taken
/// @return amountIn gross input amount to transfer (includes fee), amountOut output amount user would receive, inFee fee amount taken
function swapToLimitAmounts(
IPartyPool pool,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
int128 limitPrice
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
) external view returns (uint256 amountIn, uint256 amountOut, uint256 inFee) {
LMSRStabilized.State memory lmsr = pool.LMSR();
require(inputTokenIndex < lmsr.nAssets && outputTokenIndex < lmsr.nAssets, "swapToLimit: idx");
require(limitPrice > int128(0), "swapToLimit: limit <= 0");
@@ -105,17 +105,17 @@ contract PartyPoolViewer is PartyPoolHelpers, IPartyPoolViewer {
return SWAP_IMPL.swapToLimitAmounts(
inputTokenIndex, outputTokenIndex, limitPrice,
pool.denominators(), pool.kappa(), lmsr.qInternal, pool.swapFeePpm());
pool.denominators(), pool.kappa(), lmsr.qInternal, pool.fee(inputTokenIndex, outputTokenIndex));
}
function swapMintAmounts(IPartyPool pool, uint256 inputTokenIndex, uint256 maxAmountIn) external view
returns (uint256 amountInUsed, uint256 fee, uint256 lpMinted) {
returns (uint256 amountInUsed, uint256 lpMinted, uint256 inFee) {
LMSRStabilized.State memory lmsr = pool.LMSR();
return MINT_IMPL.swapMintAmounts(
inputTokenIndex,
maxAmountIn,
pool.swapFeePpm(),
pool.fees()[inputTokenIndex],
lmsr,
pool.denominators(),
pool.totalSupply()
@@ -124,12 +124,12 @@ contract PartyPoolViewer is PartyPoolHelpers, IPartyPoolViewer {
function burnSwapAmounts(IPartyPool pool, uint256 lpAmount, uint256 outputTokenIndex) external view
returns (uint256 amountOut) {
returns (uint256 amountOut, uint256 outFee) {
LMSRStabilized.State memory lmsr = pool.LMSR();
return MINT_IMPL.burnSwapAmounts(
lpAmount,
outputTokenIndex,
pool.swapFeePpm(),
pool.fees()[outputTokenIndex],
lmsr,
pool.denominators(),
pool.totalSupply()